fix(usage): improve MiniMax coding-plan usage parsing for model_remains array

- Pick the chat model entry (MiniMax-M*) from model_remains instead of using the first BFS candidate, which could be a speech/video/image model with total_count=0.
- Derive window label from start_time/end_time timestamps when window_hours/window_minutes fields are absent; fixes the hardcoded 5h default for 4h windows.
- Include model name in plan label so users can distinguish free-tier coding-plan quota from paid API balance.

Closes #52335
This commit is contained in:
IVY 2026-03-23 00:00:33 +08:00 committed by Peter Steinberger
parent 90af255a91
commit efd5d5eb20
1 changed files with 75 additions and 4 deletions

View File

@ -267,6 +267,23 @@ function collectUsageCandidates(root: Record<string, unknown>): Record<string, u
return candidates.map((candidate) => candidate.record);
}
function deriveWindowLabelFromTimestamps(record: Record<string, unknown>): string | undefined {
const startTime = parseEpoch(record.start_time ?? record.startTime);
const endTime = parseEpoch(record.end_time ?? record.endTime);
if (startTime && endTime && endTime > startTime) {
const durationHours = (endTime - startTime) / 3_600_000;
if (durationHours >= 1 && Number.isFinite(durationHours)) {
const rounded = Math.round(durationHours);
return `${rounded}h`;
}
const durationMinutes = Math.round((endTime - startTime) / 60_000);
if (durationMinutes > 0) {
return `${durationMinutes}m`;
}
}
return undefined;
}
function deriveWindowLabel(payload: Record<string, unknown>): string {
const hours = pickNumber(payload, WINDOW_HOUR_KEYS);
if (hours && Number.isFinite(hours)) {
@ -276,6 +293,10 @@ function deriveWindowLabel(payload: Record<string, unknown>): string {
if (minutes && Number.isFinite(minutes)) {
return `${minutes}m`;
}
const fromTimestamps = deriveWindowLabelFromTimestamps(payload);
if (fromTimestamps) {
return fromTimestamps;
}
return "5h";
}
@ -315,6 +336,40 @@ function deriveUsedPercent(payload: Record<string, unknown>): number | null {
return null;
}
/**
* When the API returns a `model_remains` array, prefer the entry whose
* `model_name` matches a chat/text model (e.g. "MiniMax-M*") and that has
* a non-zero `current_interval_total_count`. Models with total_count === 0
* (speech, video, image) are not relevant to the coding-plan budget.
*/
function pickChatModelRemains(
modelRemains: unknown[],
): Record<string, unknown> | undefined {
const records = modelRemains.filter(isRecord);
if (records.length === 0) {
return undefined;
}
const chatRecord = records.find((r) => {
const name = typeof r.model_name === "string" ? r.model_name : "";
const total = parseFiniteNumber(r.current_interval_total_count);
return (
(name.toLowerCase().startsWith("minimax-m") || name === "MiniMax-M*") &&
total !== undefined &&
total > 0
);
});
if (chatRecord) {
return chatRecord;
}
return records.find((r) => {
const total = parseFiniteNumber(r.current_interval_total_count);
return total !== undefined && total > 0;
});
}
export async function fetchMinimaxUsage(
apiKey: string,
timeoutMs: number,
@ -362,8 +417,15 @@ export async function fetchMinimaxUsage(
}
const payload = isRecord(data.data) ? data.data : data;
const candidates = collectUsageCandidates(payload);
let usageRecord: Record<string, unknown> = payload;
// Handle the model_remains array structure returned by the coding-plan
// endpoint. Pick the chat-model entry so that speech/video/image quotas
// (which often have total_count === 0) don't shadow the relevant budget.
const modelRemains = Array.isArray(payload.model_remains) ? payload.model_remains : null;
const chatRemains = modelRemains ? pickChatModelRemains(modelRemains) : undefined;
const candidates = collectUsageCandidates(chatRemains ?? payload);
let usageRecord: Record<string, unknown> = chatRemains ?? payload;
let usedPercent: number | null = null;
for (const candidate of candidates) {
const candidatePercent = deriveUsedPercent(candidate);
@ -374,7 +436,7 @@ export async function fetchMinimaxUsage(
}
}
if (usedPercent === null) {
usedPercent = deriveUsedPercent(payload);
usedPercent = deriveUsedPercent(chatRemains ?? payload);
}
if (usedPercent === null) {
return {
@ -398,10 +460,19 @@ export async function fetchMinimaxUsage(
},
];
const modelName =
chatRemains && typeof chatRemains.model_name === "string"
? chatRemains.model_name
: undefined;
const plan =
pickString(usageRecord, PLAN_KEYS) ??
pickString(payload, PLAN_KEYS) ??
(modelName ? `Coding Plan · ${modelName}` : undefined);
return {
provider: "minimax",
displayName: PROVIDER_LABELS.minimax,
windows,
plan: pickString(usageRecord, PLAN_KEYS) ?? pickString(payload, PLAN_KEYS),
plan,
};
}