Files
OmniRoute/tests/unit/model-combo-mappings.test.mjs
diegosouzapw 5dc3fd2ec0 feat: per-model combo routing support (#563)
Add model-pattern → combo mapping feature that automatically routes requests
to specific combos based on model name patterns (glob matching).

Implementation:
- New migration 010: model_combo_mappings table with pattern, combo_id, priority
- DB module with CRUD + resolveComboForModel() using glob-to-regex matching
- getComboForModel() in model.ts: augments getCombo() with pattern fallback
- chat.ts: replaced getCombo() → getComboForModel() at routing decision point
- API endpoints: GET/POST /api/model-combo-mappings, GET/PUT/DELETE by [id]
- ModelRoutingSection.tsx: dashboard UI with inline add/edit/toggle/delete
- Integrated into Combos page
- 15 new unit tests (glob matching, priority ordering, disabled filtering)
- Full test suite: 923/923 pass

Examples:
  claude-sonnet* → code-combo
  claude-*-opus* → frontier-combo
  gpt-4o*       → openai-combo
  gemini-*      → google-combo

Resolves: #563
2026-03-23 20:36:00 -03:00

136 lines
5.2 KiB
JavaScript

/**
* Unit tests for Per-Model Combo Support (#563)
* Tests glob pattern matching and priority ordering.
*/
import { test, describe } from "node:test";
import assert from "node:assert/strict";
// ──────────────────────────────────────────────────────────
// Inline glob-to-regex (same logic as modelComboMappings.ts)
// ──────────────────────────────────────────────────────────
function globToRegex(pattern) {
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".");
return new RegExp(`^${escaped}$`, "i");
}
/**
* Simulate resolveComboForModel logic in-memory.
*/
function resolveFromMappings(modelStr, mappings) {
const enabled = mappings.filter((m) => m.enabled).sort((a, b) => b.priority - a.priority);
for (const mapping of enabled) {
const regex = globToRegex(mapping.pattern);
if (regex.test(modelStr)) {
return mapping.comboName;
}
}
return null;
}
// ──────────────────────────────────────────────────────────
// Test Cases
// ──────────────────────────────────────────────────────────
describe("Model-Combo Mapping: globToRegex", () => {
test("exact match", () => {
const re = globToRegex("claude-sonnet-4");
assert.ok(re.test("claude-sonnet-4"));
assert.ok(!re.test("claude-opus-4"));
});
test("wildcard * matches any characters", () => {
const re = globToRegex("claude-*-opus*");
assert.ok(re.test("claude-sonnet-opus-4"));
assert.ok(re.test("claude-3-opus-20240229"));
assert.ok(!re.test("gpt-4o"));
});
test("wildcard ? matches single character", () => {
const re = globToRegex("gpt-4?");
assert.ok(re.test("gpt-4o"));
assert.ok(re.test("gpt-4t"));
assert.ok(!re.test("gpt-4oo"));
});
test("case-insensitive matching", () => {
const re = globToRegex("Claude-*");
assert.ok(re.test("claude-sonnet-4"));
assert.ok(re.test("CLAUDE-OPUS-4"));
});
test("escapes regex special characters", () => {
const re = globToRegex("model.v2");
assert.ok(re.test("model.v2"));
assert.ok(!re.test("modelXv2")); // . should be literal, not regex .
});
test("pattern with slash (provider/model)", () => {
const re = globToRegex("cc/claude-*");
assert.ok(re.test("cc/claude-opus-4"));
assert.ok(!re.test("gh/claude-opus-4"));
});
});
describe("Model-Combo Mapping: resolveFromMappings", () => {
const mappings = [
{ pattern: "claude-*-opus*", comboName: "frontier-combo", priority: 10, enabled: true },
{ pattern: "claude-sonnet*", comboName: "code-combo", priority: 10, enabled: true },
{ pattern: "gpt-4o*", comboName: "openai-combo", priority: 5, enabled: true },
{ pattern: "gemini-*", comboName: "google-combo", priority: 3, enabled: true },
{ pattern: "disabled-*", comboName: "disabled-combo", priority: 100, enabled: false },
{ pattern: "*", comboName: "catch-all", priority: 0, enabled: true },
];
test("matches opus pattern → frontier-combo", () => {
assert.equal(resolveFromMappings("claude-3-opus-20240229", mappings), "frontier-combo");
});
test("matches sonnet pattern → code-combo", () => {
assert.equal(resolveFromMappings("claude-sonnet-4", mappings), "code-combo");
});
test("matches gpt-4o → openai-combo", () => {
assert.equal(resolveFromMappings("gpt-4o-mini", mappings), "openai-combo");
});
test("matches gemini → google-combo", () => {
assert.equal(resolveFromMappings("gemini-2.5-flash", mappings), "google-combo");
});
test("disabled mappings are skipped", () => {
assert.notEqual(resolveFromMappings("disabled-model", mappings), "disabled-combo");
// Falls through to catch-all instead
assert.equal(resolveFromMappings("disabled-model", mappings), "catch-all");
});
test("unknown model falls to catch-all (* pattern)", () => {
assert.equal(resolveFromMappings("deepseek-chat", mappings), "catch-all");
});
test("no mappings returns null", () => {
assert.equal(resolveFromMappings("any-model", []), null);
});
test("priority ordering: higher priority wins when multiple match", () => {
const overlapping = [
{ pattern: "claude-*", comboName: "generic-claude", priority: 1, enabled: true },
{ pattern: "claude-*-opus*", comboName: "specific-opus", priority: 10, enabled: true },
];
assert.equal(resolveFromMappings("claude-3-opus-4", overlapping), "specific-opus");
});
test("priority ordering: lower priority loses even if listed first", () => {
const overlapping = [
{ pattern: "claude-*-opus*", comboName: "specific-opus", priority: 1, enabled: true },
{ pattern: "claude-*", comboName: "generic-claude", priority: 10, enabled: true },
];
// generic-claude has higher priority (10 > 1), so it wins
assert.equal(resolveFromMappings("claude-3-opus-4", overlapping), "generic-claude");
});
});