mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 15:21:06 +00:00
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user