Skip to content

Commit efd5d5e

Browse files
IVY-AI-gifsteipete
authored andcommitted
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
1 parent 90af255 commit efd5d5e

1 file changed

Lines changed: 75 additions & 4 deletions

File tree

src/infra/provider-usage.fetch.minimax.ts

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,23 @@ function collectUsageCandidates(root: Record<string, unknown>): Record<string, u
267267
return candidates.map((candidate) => candidate.record);
268268
}
269269

270+
function deriveWindowLabelFromTimestamps(record: Record<string, unknown>): string | undefined {
271+
const startTime = parseEpoch(record.start_time ?? record.startTime);
272+
const endTime = parseEpoch(record.end_time ?? record.endTime);
273+
if (startTime && endTime && endTime > startTime) {
274+
const durationHours = (endTime - startTime) / 3_600_000;
275+
if (durationHours >= 1 && Number.isFinite(durationHours)) {
276+
const rounded = Math.round(durationHours);
277+
return `${rounded}h`;
278+
}
279+
const durationMinutes = Math.round((endTime - startTime) / 60_000);
280+
if (durationMinutes > 0) {
281+
return `${durationMinutes}m`;
282+
}
283+
}
284+
return undefined;
285+
}
286+
270287
function deriveWindowLabel(payload: Record<string, unknown>): string {
271288
const hours = pickNumber(payload, WINDOW_HOUR_KEYS);
272289
if (hours && Number.isFinite(hours)) {
@@ -276,6 +293,10 @@ function deriveWindowLabel(payload: Record<string, unknown>): string {
276293
if (minutes && Number.isFinite(minutes)) {
277294
return `${minutes}m`;
278295
}
296+
const fromTimestamps = deriveWindowLabelFromTimestamps(payload);
297+
if (fromTimestamps) {
298+
return fromTimestamps;
299+
}
279300
return "5h";
280301
}
281302

@@ -315,6 +336,40 @@ function deriveUsedPercent(payload: Record<string, unknown>): number | null {
315336
return null;
316337
}
317338

339+
/**
340+
* When the API returns a `model_remains` array, prefer the entry whose
341+
* `model_name` matches a chat/text model (e.g. "MiniMax-M*") and that has
342+
* a non-zero `current_interval_total_count`. Models with total_count === 0
343+
* (speech, video, image) are not relevant to the coding-plan budget.
344+
*/
345+
function pickChatModelRemains(
346+
modelRemains: unknown[],
347+
): Record<string, unknown> | undefined {
348+
const records = modelRemains.filter(isRecord);
349+
if (records.length === 0) {
350+
return undefined;
351+
}
352+
353+
const chatRecord = records.find((r) => {
354+
const name = typeof r.model_name === "string" ? r.model_name : "";
355+
const total = parseFiniteNumber(r.current_interval_total_count);
356+
return (
357+
(name.toLowerCase().startsWith("minimax-m") || name === "MiniMax-M*") &&
358+
total !== undefined &&
359+
total > 0
360+
);
361+
});
362+
363+
if (chatRecord) {
364+
return chatRecord;
365+
}
366+
367+
return records.find((r) => {
368+
const total = parseFiniteNumber(r.current_interval_total_count);
369+
return total !== undefined && total > 0;
370+
});
371+
}
372+
318373
export async function fetchMinimaxUsage(
319374
apiKey: string,
320375
timeoutMs: number,
@@ -362,8 +417,15 @@ export async function fetchMinimaxUsage(
362417
}
363418

364419
const payload = isRecord(data.data) ? data.data : data;
365-
const candidates = collectUsageCandidates(payload);
366-
let usageRecord: Record<string, unknown> = payload;
420+
421+
// Handle the model_remains array structure returned by the coding-plan
422+
// endpoint. Pick the chat-model entry so that speech/video/image quotas
423+
// (which often have total_count === 0) don't shadow the relevant budget.
424+
const modelRemains = Array.isArray(payload.model_remains) ? payload.model_remains : null;
425+
const chatRemains = modelRemains ? pickChatModelRemains(modelRemains) : undefined;
426+
427+
const candidates = collectUsageCandidates(chatRemains ?? payload);
428+
let usageRecord: Record<string, unknown> = chatRemains ?? payload;
367429
let usedPercent: number | null = null;
368430
for (const candidate of candidates) {
369431
const candidatePercent = deriveUsedPercent(candidate);
@@ -374,7 +436,7 @@ export async function fetchMinimaxUsage(
374436
}
375437
}
376438
if (usedPercent === null) {
377-
usedPercent = deriveUsedPercent(payload);
439+
usedPercent = deriveUsedPercent(chatRemains ?? payload);
378440
}
379441
if (usedPercent === null) {
380442
return {
@@ -398,10 +460,19 @@ export async function fetchMinimaxUsage(
398460
},
399461
];
400462

463+
const modelName =
464+
chatRemains && typeof chatRemains.model_name === "string"
465+
? chatRemains.model_name
466+
: undefined;
467+
const plan =
468+
pickString(usageRecord, PLAN_KEYS) ??
469+
pickString(payload, PLAN_KEYS) ??
470+
(modelName ? `Coding Plan · ${modelName}` : undefined);
471+
401472
return {
402473
provider: "minimax",
403474
displayName: PROVIDER_LABELS.minimax,
404475
windows,
405-
plan: pickString(usageRecord, PLAN_KEYS) ?? pickString(payload, PLAN_KEYS),
476+
plan,
406477
};
407478
}

0 commit comments

Comments
 (0)