mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
fix(usage): improve Claude and MiniMax plan label detection (#2498)
Integrated into release/v3.8.2
This commit is contained in:
@@ -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,
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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_-]+/)
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
Reference in New Issue
Block a user