mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix(minimax): Bổ sung MiniMax-M3 + cập nhật Quota Tracker coding/CN
Squash-merge PR #1631 (decolua/9router) — chỉ lấy file code + test, bỏ docs. - feat(minimax): add MiniMax-M3 to intl + cn provider models (targetFormat claude) - feat(minimax): add MiniMax-M3 pricing entry - fix(minimax): translate Claude body khi content=null (M3 thinking-only) - fix(minimax): hiển thị quota M-series bucket "general"/"MiniMax-M*" + percent-only - test: minimax usage / model registration / pricing Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
decolua
co-authored by
Claude
Cursor
parent
cce8a50cac
commit
41f94ce8c8
@@ -337,6 +337,7 @@ export const PROVIDER_MODELS = {
|
||||
{ id: "kimi-latest", name: "Kimi Latest" },
|
||||
],
|
||||
minimax: [
|
||||
{ id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
|
||||
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
|
||||
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
|
||||
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
||||
@@ -363,6 +364,7 @@ export const PROVIDER_MODELS = {
|
||||
{ id: "qwen3-vl-plus", name: "Qwen3 VL Plus" },
|
||||
],
|
||||
"minimax-cn": [
|
||||
{ id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
|
||||
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
|
||||
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
|
||||
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
||||
|
||||
@@ -72,12 +72,16 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||
|
||||
// Claude
|
||||
if (targetFormat === FORMATS.CLAUDE) {
|
||||
if (!responseBody.content) return responseBody;
|
||||
// Always translate a Claude-format body to OpenAI, even if `content` is
|
||||
// missing/null (e.g. M3 with max_tokens:1 spends the budget on thinking
|
||||
// and returns `content: null`). Returning the raw body would leave the
|
||||
// OpenAI client without a `choices` array and surface as a UI test error.
|
||||
if (responseBody.content && !Array.isArray(responseBody.content)) return responseBody;
|
||||
|
||||
let textContent = "", thinkingContent = "";
|
||||
const toolCalls = [];
|
||||
|
||||
for (const block of responseBody.content) {
|
||||
for (const block of (responseBody.content || [])) {
|
||||
if (block.type === "text") {
|
||||
// Strip markdown code block markers (e.g. kimi wraps JSON in ```json...```)
|
||||
const raw = block.text ?? "";
|
||||
|
||||
@@ -995,6 +995,12 @@ function formatMiniMaxQuotaName(model) {
|
||||
const rawName = getMiniMaxModelName(model);
|
||||
if (!rawName) return "MiniMax";
|
||||
|
||||
// M3+ shared quota pool: MiniMax reports M-series as a single wildcard
|
||||
// bucket ("MiniMax-M*"). Newer responses rename it to plain "general".
|
||||
// Render both as a friendly series label rather than leaking the
|
||||
// asterisk or the vague "general" word to the UI.
|
||||
if (rawName === "MiniMax-M*" || rawName === "general") return "M-series";
|
||||
|
||||
return rawName
|
||||
.replace(/[_-]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
@@ -1005,6 +1011,15 @@ function formatMiniMaxQuotaName(model) {
|
||||
.replace(/\bHd\b/g, "HD");
|
||||
}
|
||||
|
||||
function getMiniMaxProvidedPercent(model, snakeKey, camelKey) {
|
||||
if (!model || typeof model !== "object") return null;
|
||||
const raw = model[snakeKey] ?? model[camelKey];
|
||||
if (raw === null || raw === undefined) return null;
|
||||
const num = Number(raw);
|
||||
if (!Number.isFinite(num)) return null;
|
||||
return Math.max(0, Math.min(100, num));
|
||||
}
|
||||
|
||||
function getMiniMaxSessionTotal(model) {
|
||||
return Math.max(0, Number(getMiniMaxField(model, "current_interval_total_count", "currentIntervalTotalCount")) || 0);
|
||||
}
|
||||
@@ -1014,7 +1029,12 @@ function getMiniMaxWeeklyTotal(model) {
|
||||
}
|
||||
|
||||
function hasMiniMaxQuota(model) {
|
||||
return getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0;
|
||||
// Old format has real count totals; M3-era M-series buckets ship percent-only
|
||||
// (count fields are 0) so accept those too.
|
||||
if (getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0) return true;
|
||||
if (getMiniMaxProvidedPercent(model, "current_interval_remaining_percent", "currentIntervalRemainingPercent") !== null) return true;
|
||||
if (getMiniMaxProvidedPercent(model, "current_weekly_remaining_percent", "currentWeeklyRemainingPercent") !== null) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function getMiniMaxResetAt(model, capturedAtMs, remainsSnake, remainsCamel, endSnake, endCamel) {
|
||||
@@ -1023,30 +1043,57 @@ function getMiniMaxResetAt(model, capturedAtMs, remainsSnake, remainsCamel, endS
|
||||
return parseResetTime(getMiniMaxField(model, endSnake, endCamel));
|
||||
}
|
||||
|
||||
function buildMiniMaxQuota(total, count, resetAt, countMeansRemaining) {
|
||||
function buildMiniMaxQuota(total, count, resetAt, countMeansRemaining, providedPercent = null) {
|
||||
const safeTotal = Math.max(0, total);
|
||||
const used = countMeansRemaining ? Math.max(safeTotal - count, 0) : Math.min(Math.max(0, count), safeTotal);
|
||||
const remaining = Math.max(safeTotal - used, 0);
|
||||
// M-series buckets ship percent-only (count = 0). Prefer the upstream value
|
||||
// when present, otherwise fall back to the computed percentage. When the
|
||||
// quota is unbounded (no count) and no upstream percent is available, surface
|
||||
// the percent anyway as long as it is defined.
|
||||
const remainingPercentage = providedPercentage(providedPercent, remaining, safeTotal);
|
||||
return {
|
||||
used,
|
||||
total: safeTotal,
|
||||
remaining,
|
||||
remainingPercentage: safeTotal > 0 ? Math.max(0, Math.min(100, (remaining / safeTotal) * 100)) : 0,
|
||||
remainingPercentage,
|
||||
resetAt,
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, resetArgs, countMeansRemaining) {
|
||||
function providedPercentage(provided, remaining, total) {
|
||||
if (provided !== null && provided !== undefined && Number.isFinite(provided)) {
|
||||
return Math.max(0, Math.min(100, provided));
|
||||
}
|
||||
return total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 0;
|
||||
}
|
||||
|
||||
function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, percentSnake, percentCamel, resetArgs, countMeansRemaining) {
|
||||
const total = getTotal(model);
|
||||
if (total <= 0) return;
|
||||
const providedPercent = getMiniMaxProvidedPercent(model, percentSnake, percentCamel);
|
||||
if (total <= 0 && providedPercent === null) return;
|
||||
|
||||
const count = Math.max(0, Number(getMiniMaxField(model, countSnake, countCamel)) || 0);
|
||||
let effectiveTotal = total;
|
||||
let effectiveCount = count;
|
||||
if (total <= 0) {
|
||||
// M-series bucket: API only ships *_remaining_percent (count = 0). Normalize
|
||||
// to total=100. The downstream buildMiniMaxQuota treats the count as
|
||||
// "used" or "remaining" depending on countMeansRemaining, so the synthetic
|
||||
// count has to match that semantic — otherwise the UI flips the percentage.
|
||||
effectiveTotal = 100;
|
||||
const pct = providedPercent;
|
||||
effectiveCount = countMeansRemaining
|
||||
? Math.round(effectiveTotal * (pct / 100))
|
||||
: Math.round(effectiveTotal * (1 - pct / 100));
|
||||
}
|
||||
quotas[key] = buildMiniMaxQuota(
|
||||
total,
|
||||
count,
|
||||
effectiveTotal,
|
||||
effectiveCount,
|
||||
getMiniMaxResetAt(model, ...resetArgs),
|
||||
countMeansRemaining
|
||||
countMeansRemaining,
|
||||
providedPercent
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1122,6 +1169,8 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
|
||||
getMiniMaxSessionTotal,
|
||||
"current_interval_usage_count",
|
||||
"currentIntervalUsageCount",
|
||||
"current_interval_remaining_percent",
|
||||
"currentIntervalRemainingPercent",
|
||||
[capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"],
|
||||
countMeansRemaining
|
||||
);
|
||||
@@ -1133,6 +1182,8 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
|
||||
getMiniMaxWeeklyTotal,
|
||||
"current_weekly_usage_count",
|
||||
"currentWeeklyUsageCount",
|
||||
"current_weekly_remaining_percent",
|
||||
"currentWeeklyRemainingPercent",
|
||||
[capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"],
|
||||
countMeansRemaining
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user