Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/shared/constants/modelSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,24 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
supportsTools: true,
},

// ── MiniMax M3 (1M context, 512K max output) ─────────────────────
// max output verified against MiniMax docs / OpenRouter / Artificial
// Analysis (Nov 2025 launch): 1,048,576-token context, up to 512K output.
"minimax-m3": {
maxOutputTokens: 512000,
contextWindow: 1048576,
supportsThinking: true,
supportsTools: true,
aliases: ["MiniMax-M3", "MiniMaxAI/MiniMax-M3"],
},

// ── MiniMax M2.x (200K context family) ───────────────────────────
"minimax-m2.7": {
maxOutputTokens: 131072,
contextWindow: 204800,
supportsThinking: true,
supportsTools: true,
aliases: ["MiniMax-M2.7", "MiniMaxAI/MiniMax-M2.7"],
},
"minimax-m2.5": {
maxOutputTokens: 131072,
Expand Down Expand Up @@ -356,14 +368,23 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
export function getModelSpec(modelId: string): ModelSpec | undefined {
if (MODEL_SPECS[modelId]) return MODEL_SPECS[modelId];

// Buscas por alias
// Case-insensitive lookups: upstream model ids are often capitalized
// (e.g. "MiniMax-M2.7") while specs/aliases use lowercase ids (#3141).
const lower = modelId.toLowerCase();

// Exact match (case-insensitive)
for (const [canonical, spec] of Object.entries(MODEL_SPECS)) {
if (spec.aliases?.includes(modelId)) return spec;
if (canonical.toLowerCase() === lower) return spec;
}

// Buscas por alias (case-insensitive)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Comment language inconsistency

This comment is in Portuguese ("Buscas por alias") while the rest of the codebase uses English comments. Should be "Alias lookups (case-insensitive)" for consistency.

for (const [, spec] of Object.entries(MODEL_SPECS)) {
if (spec.aliases?.some((alias) => alias.toLowerCase() === lower)) return spec;
}

// Prefix matching
// Prefix matching (case-insensitive)
for (const [key, spec] of Object.entries(MODEL_SPECS)) {
if (key !== "__default__" && modelId.startsWith(key)) return spec;
if (key !== "__default__" && lower.startsWith(key.toLowerCase())) return spec;
}

return undefined;
Comment on lines 369 to 390

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performance & Simplicity Optimization for getModelSpec

The current implementation of getModelSpec performs multiple O(N) loops and repeatedly calls .toLowerCase() on keys and aliases on every single lookup. Since getModelSpec is called frequently during routing and translation, we can optimize this to O(1) for exact and alias lookups:

  1. Direct O(1) Lookup: Since all keys in MODEL_SPECS are already lowercase, we can perform a direct MODEL_SPECS[lower] lookup. This completely eliminates the need for the case-insensitive exact match loop.
  2. O(1) Alias Lookup: We can lazy-initialize an alias cache on the function object itself to make alias lookups O(1) instead of looping and calling .toLowerCase() repeatedly.
  3. Optimized Prefix Matching: In the prefix matching loop, we can avoid calling key.toLowerCase() because all keys are already lowercase.
  const lower = modelId.toLowerCase();
  if (MODEL_SPECS[lower]) return MODEL_SPECS[lower];

  // Lazy-initialize alias cache for fast O(1) lookups
  let aliasCache = (getModelSpec as any).aliasCache as Record<string, ModelSpec> | undefined;
  if (!aliasCache) {
    aliasCache = {};
    for (const spec of Object.values(MODEL_SPECS)) {
      if (spec.aliases) {
        for (const alias of spec.aliases) {
          aliasCache[alias.toLowerCase()] = spec;
        }
      }
    }
    (getModelSpec as any).aliasCache = aliasCache;
  }

  const fromAlias = aliasCache[lower];
  if (fromAlias) return fromAlias;

  // Prefix matching (case-insensitive)
  for (const [key, spec] of Object.entries(MODEL_SPECS)) {
    if (key !== "__default__" && lower.startsWith(key)) return spec;
  }

  return undefined;

Expand Down
80 changes: 80 additions & 0 deletions tests/unit/minimax-m3-maxtokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* tests/unit/minimax-m3-maxtokens.test.ts
*
* Regression for issue #3141 — "Omniroute sets max_tokens = 8192 for
* minimax/MiniMax-M3" (reporter @totaltube).
*
* minimax-coding / minimax route through the Claude translator, which calls
* fitThinkingToMaxTokens(model, callerMaxTokens, …) → capMaxOutputTokens().
* The caller's larger max_tokens was silently capped to the 8192
* MODEL_SPECS.__default__ ceiling because:
* (a) MiniMax-M3 had no MODEL_SPECS entry at all, and the registry models
* declare no maxOutputTokens, so resolution fell through to __default__;
* (b) MiniMax-M2.7 (the upstream, capitalized id) never matched its existing
* lowercase "minimax-m2.7" spec because getModelSpec was case-SENSITIVE.
*
* Both lookups must now resolve to the real family ceiling (well above 8192).
*/

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-minimax-m3-"));
process.env.DATA_DIR = TEST_DATA_DIR;

const core = await import("../../src/lib/db/core.ts");
const modelCapabilities = await import("../../src/lib/modelCapabilities.ts");
const { getModelSpec } = await import("../../src/shared/constants/modelSpecs.ts");

test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});

const DEFAULT_CAP = 8192;

test("#3141 MiniMax-M3 max_tokens is not capped to the 8192 default", () => {
const cap = modelCapabilities.capMaxOutputTokens({ provider: "minimax", model: "MiniMax-M3" });
assert.ok(
cap > DEFAULT_CAP,
`expected MiniMax-M3 maxOutputTokens > ${DEFAULT_CAP}, got ${cap}`
);
});

test("#3141 MiniMaxAI/MiniMax-M3 (prefixed id) resolves above the 8192 default", () => {
const cap = modelCapabilities.capMaxOutputTokens({
provider: "minimax",
model: "MiniMaxAI/MiniMax-M3",
});
assert.ok(
cap > DEFAULT_CAP,
`expected MiniMaxAI/MiniMax-M3 maxOutputTokens > ${DEFAULT_CAP}, got ${cap}`
);
});

test("#3141 capitalized MiniMax-M2.7 resolves to its lowercase spec (case-insensitive)", () => {
const cap = modelCapabilities.capMaxOutputTokens({ provider: "minimax", model: "MiniMax-M2.7" });
assert.ok(
cap > DEFAULT_CAP,
`expected MiniMax-M2.7 maxOutputTokens > ${DEFAULT_CAP}, got ${cap}`
);
});

test("#3141 lowercase minimax-m2.7 spec is unchanged", () => {
const cap = modelCapabilities.capMaxOutputTokens({ provider: "minimax", model: "minimax-m2.7" });
assert.ok(
cap > DEFAULT_CAP,
`expected minimax-m2.7 maxOutputTokens > ${DEFAULT_CAP}, got ${cap}`
);
});

test("#3141 getModelSpec resolves MiniMax-M3 and capitalized M2.7 to family specs", () => {
assert.ok(getModelSpec("MiniMax-M3"), "MiniMax-M3 should have a spec");
assert.ok(
getModelSpec("MiniMax-M2.7"),
"capitalized MiniMax-M2.7 should resolve to the minimax-m2.7 spec"
);
});