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:
parent
cce8a50cac
commit
41f94ce8c8
7 changed files with 272 additions and 10 deletions
|
|
@ -337,6 +337,7 @@ export const PROVIDER_MODELS = {
|
||||||
{ id: "kimi-latest", name: "Kimi Latest" },
|
{ id: "kimi-latest", name: "Kimi Latest" },
|
||||||
],
|
],
|
||||||
minimax: [
|
minimax: [
|
||||||
|
{ id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
|
||||||
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
|
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
|
||||||
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
|
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
|
||||||
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
||||||
|
|
@ -363,6 +364,7 @@ export const PROVIDER_MODELS = {
|
||||||
{ id: "qwen3-vl-plus", name: "Qwen3 VL Plus" },
|
{ id: "qwen3-vl-plus", name: "Qwen3 VL Plus" },
|
||||||
],
|
],
|
||||||
"minimax-cn": [
|
"minimax-cn": [
|
||||||
|
{ id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
|
||||||
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
|
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
|
||||||
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
|
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
|
||||||
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,16 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||||
|
|
||||||
// Claude
|
// Claude
|
||||||
if (targetFormat === FORMATS.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 = "";
|
let textContent = "", thinkingContent = "";
|
||||||
const toolCalls = [];
|
const toolCalls = [];
|
||||||
|
|
||||||
for (const block of responseBody.content) {
|
for (const block of (responseBody.content || [])) {
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
// Strip markdown code block markers (e.g. kimi wraps JSON in ```json...```)
|
// Strip markdown code block markers (e.g. kimi wraps JSON in ```json...```)
|
||||||
const raw = block.text ?? "";
|
const raw = block.text ?? "";
|
||||||
|
|
|
||||||
|
|
@ -995,6 +995,12 @@ function formatMiniMaxQuotaName(model) {
|
||||||
const rawName = getMiniMaxModelName(model);
|
const rawName = getMiniMaxModelName(model);
|
||||||
if (!rawName) return "MiniMax";
|
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
|
return rawName
|
||||||
.replace(/[_-]+/g, " ")
|
.replace(/[_-]+/g, " ")
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
|
|
@ -1005,6 +1011,15 @@ function formatMiniMaxQuotaName(model) {
|
||||||
.replace(/\bHd\b/g, "HD");
|
.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) {
|
function getMiniMaxSessionTotal(model) {
|
||||||
return Math.max(0, Number(getMiniMaxField(model, "current_interval_total_count", "currentIntervalTotalCount")) || 0);
|
return Math.max(0, Number(getMiniMaxField(model, "current_interval_total_count", "currentIntervalTotalCount")) || 0);
|
||||||
}
|
}
|
||||||
|
|
@ -1014,7 +1029,12 @@ function getMiniMaxWeeklyTotal(model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMiniMaxQuota(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) {
|
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));
|
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 safeTotal = Math.max(0, total);
|
||||||
const used = countMeansRemaining ? Math.max(safeTotal - count, 0) : Math.min(Math.max(0, count), safeTotal);
|
const used = countMeansRemaining ? Math.max(safeTotal - count, 0) : Math.min(Math.max(0, count), safeTotal);
|
||||||
const remaining = Math.max(safeTotal - used, 0);
|
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 {
|
return {
|
||||||
used,
|
used,
|
||||||
total: safeTotal,
|
total: safeTotal,
|
||||||
remaining,
|
remaining,
|
||||||
remainingPercentage: safeTotal > 0 ? Math.max(0, Math.min(100, (remaining / safeTotal) * 100)) : 0,
|
remainingPercentage,
|
||||||
resetAt,
|
resetAt,
|
||||||
unlimited: false,
|
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);
|
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);
|
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(
|
quotas[key] = buildMiniMaxQuota(
|
||||||
total,
|
effectiveTotal,
|
||||||
count,
|
effectiveCount,
|
||||||
getMiniMaxResetAt(model, ...resetArgs),
|
getMiniMaxResetAt(model, ...resetArgs),
|
||||||
countMeansRemaining
|
countMeansRemaining,
|
||||||
|
providedPercent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1122,6 +1169,8 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
|
||||||
getMiniMaxSessionTotal,
|
getMiniMaxSessionTotal,
|
||||||
"current_interval_usage_count",
|
"current_interval_usage_count",
|
||||||
"currentIntervalUsageCount",
|
"currentIntervalUsageCount",
|
||||||
|
"current_interval_remaining_percent",
|
||||||
|
"currentIntervalRemainingPercent",
|
||||||
[capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"],
|
[capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"],
|
||||||
countMeansRemaining
|
countMeansRemaining
|
||||||
);
|
);
|
||||||
|
|
@ -1133,6 +1182,8 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
|
||||||
getMiniMaxWeeklyTotal,
|
getMiniMaxWeeklyTotal,
|
||||||
"current_weekly_usage_count",
|
"current_weekly_usage_count",
|
||||||
"currentWeeklyUsageCount",
|
"currentWeeklyUsageCount",
|
||||||
|
"current_weekly_remaining_percent",
|
||||||
|
"currentWeeklyRemainingPercent",
|
||||||
[capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"],
|
[capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"],
|
||||||
countMeansRemaining
|
countMeansRemaining
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ export const MODEL_PRICING = {
|
||||||
"glm-5": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
|
"glm-5": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 },
|
||||||
|
|
||||||
// === MiniMax ===
|
// === MiniMax ===
|
||||||
|
"MiniMax-M3": { input: 0.30, output: 1.20, cached: 0.06, reasoning: 1.80, cache_creation: 0.30 },
|
||||||
"MiniMax-M2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
"MiniMax-M2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||||
"MiniMax-M2.5": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
"MiniMax-M2.5": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||||
"MiniMax-M2.7": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
"MiniMax-M2.7": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 },
|
||||||
|
|
|
||||||
|
|
@ -113,4 +113,127 @@ describe("MiniMax usage", () => {
|
||||||
expect(usage.quotas["Music 2.6 (5h)"].used).toBe(5);
|
expect(usage.quotas["Music 2.6 (5h)"].used).toBe(5);
|
||||||
expect(usage.quotas["Image 01 (5h)"].used).toBe(2);
|
expect(usage.quotas["Image 01 (5h)"].used).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes M-series percent-only buckets that have no count totals", async () => {
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(
|
||||||
|
usageResponse([
|
||||||
|
{
|
||||||
|
model_name: "general",
|
||||||
|
current_interval_remaining_percent: 70,
|
||||||
|
current_weekly_remaining_percent: 64,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "minimax",
|
||||||
|
apiKey: "test-key",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(usage.message).toBeUndefined();
|
||||||
|
expect(usage.quotas["M-series (5h)"]).toMatchObject({
|
||||||
|
used: 30,
|
||||||
|
total: 100,
|
||||||
|
remaining: 70,
|
||||||
|
remainingPercentage: 70,
|
||||||
|
});
|
||||||
|
expect(usage.quotas["M-series (7d)"]).toMatchObject({
|
||||||
|
used: 36,
|
||||||
|
total: 100,
|
||||||
|
remaining: 64,
|
||||||
|
remainingPercentage: 64,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes M-series percent-only buckets on the coding_plan (countMeansRemaining) endpoint too", async () => {
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(
|
||||||
|
usageResponse([
|
||||||
|
{
|
||||||
|
model_name: "general",
|
||||||
|
current_interval_remaining_percent: 80,
|
||||||
|
current_weekly_remaining_percent: 95,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "minimax-cn",
|
||||||
|
apiKey: "test-key",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(usage.message).toBeUndefined();
|
||||||
|
expect(usage.quotas["M-series (5h)"]).toMatchObject({
|
||||||
|
used: 20,
|
||||||
|
total: 100,
|
||||||
|
remaining: 80,
|
||||||
|
remainingPercentage: 80,
|
||||||
|
});
|
||||||
|
expect(usage.quotas["M-series (7d)"]).toMatchObject({
|
||||||
|
used: 5,
|
||||||
|
total: 100,
|
||||||
|
remaining: 95,
|
||||||
|
remainingPercentage: 95,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the M3-era MiniMax-M* wildcard as a friendly series label", async () => {
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(
|
||||||
|
usageResponse([
|
||||||
|
{
|
||||||
|
modelName: "MiniMax-M*",
|
||||||
|
currentIntervalTotalCount: 4000,
|
||||||
|
currentIntervalUsageCount: 3200,
|
||||||
|
currentWeeklyTotalCount: 24000,
|
||||||
|
currentWeeklyUsageCount: 18000,
|
||||||
|
remainsTime: 1000,
|
||||||
|
weeklyRemainsTime: 2000,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "minimax-cn",
|
||||||
|
apiKey: "test-key",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(usage.message).toBeUndefined();
|
||||||
|
expect(Object.keys(usage.quotas)).toEqual([
|
||||||
|
"M-series (5h)",
|
||||||
|
"M-series (7d)",
|
||||||
|
]);
|
||||||
|
expect(usage.quotas["M-series (5h)"]).toMatchObject({
|
||||||
|
used: 800,
|
||||||
|
total: 4000,
|
||||||
|
remaining: 3200,
|
||||||
|
});
|
||||||
|
expect(usage.quotas["M-series (7d)"]).toMatchObject({
|
||||||
|
used: 6000,
|
||||||
|
total: 24000,
|
||||||
|
remaining: 18000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers the upstream-provided remaining percent when counts are also present", async () => {
|
||||||
|
proxyAwareFetch.mockResolvedValueOnce(
|
||||||
|
usageResponse([
|
||||||
|
{
|
||||||
|
model_name: "general",
|
||||||
|
current_interval_total_count: 4000,
|
||||||
|
current_interval_usage_count: 100,
|
||||||
|
current_interval_remaining_percent: 84,
|
||||||
|
current_weekly_total_count: 24000,
|
||||||
|
current_weekly_usage_count: 500,
|
||||||
|
current_weekly_remaining_percent: 42,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const usage = await getUsageForProvider({
|
||||||
|
provider: "minimax",
|
||||||
|
apiKey: "test-key",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(usage.quotas["M-series (5h)"].remainingPercentage).toBe(84);
|
||||||
|
expect(usage.quotas["M-series (7d)"].remainingPercentage).toBe(42);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
52
tests/unit/provider-models-minimax-m3.test.js
Normal file
52
tests/unit/provider-models-minimax-m3.test.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Unit tests verifying MiniMax-M3 is registered as a first-class
|
||||||
|
* built-in model for both the `minimax` (international) and
|
||||||
|
* `minimax-cn` (China) providers, with `targetFormat: "claude"`.
|
||||||
|
*
|
||||||
|
* Run: cd tests && NODE_PATH=/tmp/node_modules /tmp/node_modules/.bin/vitest run tests/unit/provider-models-minimax-m3.test.js --reporter=verbose
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { PROVIDER_MODELS, getModelsByProviderId } from "../../open-sse/config/providerModels.js";
|
||||||
|
|
||||||
|
describe("MiniMax-M3 model registration", () => {
|
||||||
|
it("includes MiniMax-M3 in PROVIDER_MODELS.minimax", () => {
|
||||||
|
const models = PROVIDER_MODELS.minimax || [];
|
||||||
|
const m3 = models.find((m) => m.id === "MiniMax-M3");
|
||||||
|
expect(m3).toBeDefined();
|
||||||
|
expect(m3).toMatchObject({
|
||||||
|
id: "MiniMax-M3",
|
||||||
|
name: "MiniMax M3",
|
||||||
|
targetFormat: "claude",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes MiniMax-M3 in PROVIDER_MODELS['minimax-cn']", () => {
|
||||||
|
const models = PROVIDER_MODELS["minimax-cn"] || [];
|
||||||
|
const m3 = models.find((m) => m.id === "MiniMax-M3");
|
||||||
|
expect(m3).toBeDefined();
|
||||||
|
expect(m3).toMatchObject({
|
||||||
|
id: "MiniMax-M3",
|
||||||
|
name: "MiniMax M3",
|
||||||
|
targetFormat: "claude",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes MiniMax-M3 through getModelsByProviderId for both provider IDs", () => {
|
||||||
|
const intlModels = getModelsByProviderId("minimax");
|
||||||
|
const cnModels = getModelsByProviderId("minimax-cn");
|
||||||
|
|
||||||
|
expect(intlModels.some((m) => m.id === "MiniMax-M3")).toBe(true);
|
||||||
|
expect(cnModels.some((m) => m.id === "MiniMax-M3")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not regress the existing M2.7 / M2.5 / M2.1 entries", () => {
|
||||||
|
const intlIds = (PROVIDER_MODELS.minimax || []).map((m) => m.id);
|
||||||
|
const cnIds = (PROVIDER_MODELS["minimax-cn"] || []).map((m) => m.id);
|
||||||
|
|
||||||
|
for (const id of ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1"]) {
|
||||||
|
expect(intlIds).toContain(id);
|
||||||
|
expect(cnIds).toContain(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
29
tests/unit/provider-pricing-minimax-m3.test.js
Normal file
29
tests/unit/provider-pricing-minimax-m3.test.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { MODEL_PRICING } from "../../src/shared/constants/pricing.js";
|
||||||
|
|
||||||
|
describe("MiniMax-M3 pricing", () => {
|
||||||
|
it("includes MiniMax-M3 in MODEL_PRICING", () => {
|
||||||
|
expect(MODEL_PRICING["MiniMax-M3"]).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("MiniMax-M3 pricing has numeric shape (input, output, cached)", () => {
|
||||||
|
const pricing = MODEL_PRICING["MiniMax-M3"];
|
||||||
|
expect(pricing).toMatchObject({
|
||||||
|
input: expect.any(Number),
|
||||||
|
output: expect.any(Number),
|
||||||
|
cached: expect.any(Number),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("MiniMax-M3 input price matches the design spec (0.30)", () => {
|
||||||
|
expect(MODEL_PRICING["MiniMax-M3"].input).toBe(0.30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("MiniMax-M3 output price matches the design spec (1.20)", () => {
|
||||||
|
expect(MODEL_PRICING["MiniMax-M3"].output).toBe(1.20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("MiniMax-M3 cached price matches the design spec (0.06)", () => {
|
||||||
|
expect(MODEL_PRICING["MiniMax-M3"].cached).toBe(0.06);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue