diff --git a/packages/agentcompass/package.json b/packages/agentcompass/package.json index d7573247d..d7c506f77 100644 --- a/packages/agentcompass/package.json +++ b/packages/agentcompass/package.json @@ -1,6 +1,6 @@ { "name": "@magpie-community/gateway-agentcompass", - "version": "0.1.3", + "version": "0.1.4", "description": "Intent, multi-dimensional capability, Model DNA, subscription quota opportunity-cost, and session-affinity routing plugin for Magpie, powered by AgentCompass.", "type": "module", "main": "./src/index.js", diff --git a/packages/agentcompass/src/catalog.js b/packages/agentcompass/src/catalog.js index e26fdc6ae..122983a21 100644 --- a/packages/agentcompass/src/catalog.js +++ b/packages/agentcompass/src/catalog.js @@ -1506,9 +1506,13 @@ export function canonicalModel(rawModel, aliases = MODEL_ALIASES) { if (aliases[clean]) return aliases[clean]; const slashIdx = clean.indexOf("/"); const unprefixed = slashIdx >= 0 ? clean.slice(slashIdx + 1) : clean; - if (aliases[unprefixed]) return aliases[unprefixed]; - const lower = unprefixed.toLowerCase(); - if (aliases[lower]) return aliases[lower]; + // A multi-prefix id (`openrouter/openai/gpt-5-mini`) also resolves by its last segment, as AgentCompass's + // `resolve_canonical_model` does. An unknown id keeps only its first prefix stripped (`openrouter/qwen/foo` -> + // `qwen/foo`), the key quota.js and continuity.js already match on. + const last = clean.slice(clean.lastIndexOf("/") + 1); + for (const name of [unprefixed, unprefixed.toLowerCase(), last, last.toLowerCase()]) { + if (aliases[name]) return aliases[name]; + } return unprefixed; } diff --git a/packages/agentcompass/test/router.test.mjs b/packages/agentcompass/test/router.test.mjs index 1a21eaf46..d2d57b90c 100644 --- a/packages/agentcompass/test/router.test.mjs +++ b/packages/agentcompass/test/router.test.mjs @@ -6,7 +6,7 @@ import test from "node:test"; import { fileURLToPath } from "node:url"; import { burnScale, effortValue, namedRoute, rankCandidates, selectBestRoute } from "../src/allocator.js"; -import { CANONICAL_ROUTES, DEFAULT_SCORING_POLICY, EFFORT_CURVES } from "../src/catalog.js"; +import { CANONICAL_ROUTES, DEFAULT_SCORING_POLICY, EFFORT_CURVES, canonicalModel } from "../src/catalog.js"; import { CACHE_COLD_MS, CHATGPT_CODEX_COLD_MS, @@ -2181,6 +2181,14 @@ test("9.7 PR #670 Round 2 Guard: Mainstream real model IDs & trivial single-vari `Expected claude-opus-4-6:(high|xhigh|max) for permutation [${candidateModels.join(", ")}], got ${auditOut.model}` ); } + + // A multi-prefix id resolves by its last segment; an unknown one keeps only its first prefix stripped + assert.equal(canonicalModel("openrouter/openai/gpt-5-mini"), "gpt-6-luna"); + assert.equal(canonicalModel("OpenRouter/OpenAI/GPT-5-Mini"), "gpt-6-luna"); + // Alias keys with capitals (`MiniMax-M3`) need the exact-case last-segment lookup + assert.equal(canonicalModel("openrouter/minimax/MiniMax-M3"), "minimax-m3"); + assert.equal(canonicalModel("openrouter/qwen/foo"), "qwen/foo"); + assert.equal(canonicalModel("qwen/foo"), "foo"); }); test("9.8 PR #670 Round 2 Guard: `options.fetch` alone never enables external scorer unless `allowExternalScorer === true`", async () => {