mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
refactor(routing): D17 — alias-aware getProviderForModel as routing SPOT (Findings 12 + 13)
cold-audit catch from 2026-05-23
Cold-audit Findings 12 + 13 (both P3, both routing layer). F12: mistral
plugin's models[] included canonical IDs + aliases while anthropic/codex
were canonical-only — inconsistent routing surface (request "sonnet"
returned 503 from anthropic but request "devstral" worked for mistral).
F13: getProviderForModel imported but never called; buildDefaultChain
duplicated the lookup loop — two SPOT-candidate code paths.
Per cold-audit reviewer's option (a): standardize models[] on canonical-
only across all 3 plugins; getProviderForModel becomes alias-aware via
models-registry.json; buildDefaultChain uses getProviderForModel as SPOT.
Changes (4 files, +242 / -50):
1. lib/providers/index.mjs:
- At module load, builds `_aliasMap: Map<aliasString, {providerName,
canonicalModel}>` by walking models-registry.json's providers[*].aliases
- getProviderForModel signature extended: returns `{provider, name,
canonicalModel}` (was `{provider, name}`). The canonicalModel field is
the resolved canonical ID for callers that need to use it downstream
(cache key, observability headers, log events)
- 3-step resolution order: (1) alias map lookup → if hit AND provider
loaded → return with canonicalModel; (2) direct canonical scan → return
with canonicalModel = modelString; (3) null
- "Alias known but provider not loaded" case correctly falls through
to step 2 (which will also miss for an alias string) → returns null
2. lib/providers/mistral.mjs:
- _registryModels stripped to canonical-only: removed the
`...Object.keys(_registryEntry.aliases ?? {})` spread
- mistral.models[] now matches anthropic/codex shape (canonical IDs only)
- Comment block updated to point future readers at getProviderForModel
as the SPOT
3. lib/fallback/engine.mjs:
- buildDefaultChain's inline `for ([name, provider] of loadedProviders)`
scan loop replaced with a single getProviderForModel() call
- Chain hop's `model` field is now `match.canonicalModel` (was `modelString`)
— so downstream consumers receive canonical
- Explicit-chain config path unchanged (kept its pre-existing behavior;
alias resolution in routing.chains config is a known limitation, tracked
as a future improvement)
4. test-features.mjs:
- 17 new tests in `D17 — alias-aware getProviderForModel` describe block:
anthropic alias coverage (sonnet/opus/haiku/claude), openai aliases
(codex/codex-spark/gpt5/gpt5-mini), mistral aliases (devstral/
devstral-2/devstral-small/devstral-small-2), canonical pass-through,
unknown model → null, alias-to-disabled-provider → null,
buildDefaultChain integration with alias resolution
- 3 existing mistral tests rewritten — they were asserting the pre-D17
inconsistent shape (aliases in mistral.models[]). Now assert the
post-D17 invariant (aliases NOT in models[]; routing via
getProviderForModel instead)
Tests: 300 → 317 (+17 new). All pass on Node 20.
Pre-commit fold-in (per evidence-first checkpoint #4 — fold-ins themselves
need second-pass review):
- **D17 reviewer flagged C10**: original engine.mjs comment said
"downstream (cache key, X-OLP-Model-Used, provider.spawn) receives the
canonical ID rather than the alias string". The provider.spawn portion
is incorrect — spawn reads `irRequest.model` (the user's original input),
which is NEVER rewritten to canonical. Each provider CLI accepts its own
aliases natively (claude accepts sonnet/opus/haiku; vibe accepts
devstral/devstral-2; codex accepts its model IDs), so runtime behavior
is fine, but the comment overstated what D17 actually changes.
Folded in: corrected comment to honestly describe the canonical flow
(cache key + X-OLP-Model-Used + logs receive canonical; spawn continues
to receive irRequest.model). Same class of doc-code drift fix as D11's
B1 (false maxConcurrent enforcement claim) and D16's "bypasses
getOrCompute" drift — the discipline is maturing across D-days.
Authority:
- ADR 0002 § Provider contract (`models: string[]` is "models this provider
serves")
https://github.com/dtzp555-max/olp/blob/main/docs/adr/0002-plugin-architecture.md
- models-registry.json — single source of truth for (provider, model)
metadata + alias→canonical mappings per AGENTS.md SPOT policy
- AGENTS.md § Project-specific constraints — "models-registry.json is the
only place to add/edit (provider, model) metadata"
- CC 开发铁律 v1.6 § 10.x — Cold Audit Findings 12 + 13
Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus, independent
of drafter): APPROVE_WITH_MINOR. Highest-value verification: walked
chain[0].model from buildDefaultChain through all downstream consumers
(computeCacheKey, X-OLP-Model-Used, log events) to confirm canonical
flow; then verified provider.spawn paths in anthropic.mjs:231 and
codex.mjs:248 read irRequest.model (user input), confirming the comment
overstatement (now fixed). Verified no alias-canonical collision exists
in current registry (12 aliases vs 10 canonical IDs, zero intersection).
Verified empty-registry edge case + provider-with-model-not-in-registry
edge case.
Follow-up items (reviewer's non-blocking observations, NOT in this PR):
- server.mjs:32 dead import of getProviderForModel — defer to D19 cleanup
- explicit-chain config path doesn't run alias resolution (routing.chains
in ~/.olp/config.json) — pre-existing limitation, file as future issue
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+19
-10
@@ -18,6 +18,7 @@ import { homedir } from 'node:os';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { ProviderError } from '../providers/base.mjs';
|
import { ProviderError } from '../providers/base.mjs';
|
||||||
|
import { getProviderForModel } from '../providers/index.mjs';
|
||||||
|
|
||||||
// ── Hard-trigger evaluation ────────────────────────────────────────────────
|
// ── Hard-trigger evaluation ────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -418,16 +419,24 @@ export function buildDefaultChain(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// No explicit chain: find first loaded provider that serves this model
|
// No explicit chain: use getProviderForModel (SPOT for alias-aware routing).
|
||||||
for (const [name, provider] of loadedProviders.entries()) {
|
// D17 Finding 13 fix: replaced the duplicated inline scan loop with this
|
||||||
if (provider.models && provider.models.includes(modelString)) {
|
// single call so alias resolution and provider lookup live in one place.
|
||||||
return [{
|
// result.canonicalModel carries the canonical ID even when user passed an
|
||||||
provider: name,
|
// alias. The canonical ID flows into: cache key (computeCacheKey arg),
|
||||||
model: modelString,
|
// X-OLP-Model-Used header, and structured log events — so identical requests
|
||||||
softTriggers: softTriggerConfig[name] ?? null,
|
// differing only in alias-vs-canonical now hit the same cache entry.
|
||||||
quotaSnapshot: null,
|
// provider.spawn continues to receive irRequest.model (the user's original
|
||||||
}];
|
// input); each provider CLI accepts its own aliases natively per its docs,
|
||||||
}
|
// so no in-IR rewrite is needed for spawn correctness.
|
||||||
|
const match = getProviderForModel(loadedProviders, modelString);
|
||||||
|
if (match) {
|
||||||
|
return [{
|
||||||
|
provider: match.name,
|
||||||
|
model: match.canonicalModel,
|
||||||
|
softTriggers: softTriggerConfig[match.name] ?? null,
|
||||||
|
quotaSnapshot: null,
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
// No provider found for this model
|
// No provider found for this model
|
||||||
|
|||||||
+53
-6
@@ -22,12 +22,17 @@
|
|||||||
* after D-later E2E audit. Authority: ADR 0006 Tier D classification for
|
* after D-later E2E audit. Authority: ADR 0006 Tier D classification for
|
||||||
* Mistral Vibe (Le Chat Pro); docs-based authority from
|
* Mistral Vibe (Le Chat Pro); docs-based authority from
|
||||||
* https://docs.mistral.ai/mistral-vibe/terminal/quickstart.
|
* https://docs.mistral.ai/mistral-vibe/terminal/quickstart.
|
||||||
|
*
|
||||||
|
* D17 (Finding 12+13): alias resolution is centralised in getProviderForModel().
|
||||||
|
* models-registry.json is the single source of truth for alias→canonical mapping.
|
||||||
|
* All provider plugins expose canonical IDs only in their models[] field.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { validateProvider } from './base.mjs';
|
import { validateProvider } from './base.mjs';
|
||||||
import anthropicDefault from './anthropic.mjs';
|
import anthropicDefault from './anthropic.mjs';
|
||||||
import codexDefault from './codex.mjs';
|
import codexDefault from './codex.mjs';
|
||||||
import mistralDefault from './mistral.mjs';
|
import mistralDefault from './mistral.mjs';
|
||||||
|
import modelsRegistryRaw from '../../models-registry.json' with { type: 'json' };
|
||||||
|
|
||||||
// Normalize default export pattern
|
// Normalize default export pattern
|
||||||
const anthropic = anthropicDefault;
|
const anthropic = anthropicDefault;
|
||||||
@@ -42,6 +47,28 @@ const STATIC_REGISTRY = [
|
|||||||
mistral,
|
mistral,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Alias map (built once at module load) ─────────────────────────────────
|
||||||
|
// Maps aliasString → { providerName: string, canonicalModel: string }
|
||||||
|
// Sourced from models-registry.json providers[*].aliases — the SPOT for alias
|
||||||
|
// definitions per D17 Finding 12 fix. Each provider plugin's models[] contains
|
||||||
|
// only canonical IDs; this map is consulted by getProviderForModel() to resolve
|
||||||
|
// alias strings before the direct-lookup fallback.
|
||||||
|
//
|
||||||
|
// Example entries:
|
||||||
|
// 'sonnet' → { providerName: 'anthropic', canonicalModel: 'claude-sonnet-4-6' }
|
||||||
|
// 'devstral' → { providerName: 'mistral', canonicalModel: 'devstral-2-25-12' }
|
||||||
|
// 'codex' → { providerName: 'openai', canonicalModel: 'gpt-5.3-codex' }
|
||||||
|
const _aliasMap = new Map();
|
||||||
|
for (const [providerName, providerEntry] of Object.entries(
|
||||||
|
modelsRegistryRaw?.providers ?? {},
|
||||||
|
)) {
|
||||||
|
for (const [alias, canonicalModel] of Object.entries(
|
||||||
|
providerEntry?.aliases ?? {},
|
||||||
|
)) {
|
||||||
|
_aliasMap.set(alias, { providerName, canonicalModel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Registry functions ────────────────────────────────────────────────────
|
// ── Registry functions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,22 +98,42 @@ export function loadProviders(config = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the first provider in `loadedProviders` whose `models[]` includes
|
* Returns the provider in `loadedProviders` that serves `modelString`, with
|
||||||
* the exact `modelString`. Returns null if no provider is found.
|
* alias resolution. D17 Finding 12+13: this is the single lookup point (SPOT)
|
||||||
|
* for model→provider routing. All callers (including buildDefaultChain) must
|
||||||
|
* use this function instead of duplicating the scan loop.
|
||||||
*
|
*
|
||||||
* This is the naive lookup strategy for D4. Phase 2+ may add prefix/alias
|
* Resolution order:
|
||||||
* matching once models-registry.json is consulted per AGENTS.md SPOT policy.
|
* 1. Alias lookup: if modelString is a known alias in models-registry.json
|
||||||
|
* AND the resolved provider is in loadedProviders (enabled), return the
|
||||||
|
* match with canonicalModel set to the canonical ID.
|
||||||
|
* 2. Direct lookup: scan loadedProviders for any p.models.includes(modelString)
|
||||||
|
* (handles canonical IDs passed directly). Returns canonicalModel=modelString.
|
||||||
|
* 3. null — no provider found.
|
||||||
*
|
*
|
||||||
* @param {Map<string, import('./base.mjs').ProviderContractV1>} loadedProviders
|
* @param {Map<string, import('./base.mjs').ProviderContractV1>} loadedProviders
|
||||||
* @param {string} modelString
|
* @param {string} modelString
|
||||||
* @returns {{ provider: import('./base.mjs').ProviderContractV1, name: string }|null}
|
* @returns {{ provider: import('./base.mjs').ProviderContractV1, name: string, canonicalModel: string }|null}
|
||||||
*/
|
*/
|
||||||
export function getProviderForModel(loadedProviders, modelString) {
|
export function getProviderForModel(loadedProviders, modelString) {
|
||||||
|
// Step 1: alias resolution via models-registry.json
|
||||||
|
const aliasEntry = _aliasMap.get(modelString);
|
||||||
|
if (aliasEntry) {
|
||||||
|
const { providerName, canonicalModel } = aliasEntry;
|
||||||
|
const provider = loadedProviders.get(providerName);
|
||||||
|
if (provider) {
|
||||||
|
return { provider, name: providerName, canonicalModel };
|
||||||
|
}
|
||||||
|
// Alias exists but target provider is not loaded (not enabled) — fall through.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: direct canonical lookup
|
||||||
for (const [name, p] of loadedProviders) {
|
for (const [name, p] of loadedProviders) {
|
||||||
if (p.models.includes(modelString)) {
|
if (p.models.includes(modelString)) {
|
||||||
return { provider: p, name };
|
return { provider: p, name, canonicalModel: modelString };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -742,19 +742,13 @@ function _defaultBinaryExists() {
|
|||||||
|
|
||||||
import modelsRegistryRaw from '../../models-registry.json' with { type: 'json' };
|
import modelsRegistryRaw from '../../models-registry.json' with { type: 'json' };
|
||||||
|
|
||||||
// Build the `models` array from BOTH canonical date-stamped IDs and the
|
// Build the `models` array from canonical date-stamped IDs only — matching the
|
||||||
// short-form aliases per registry. ADR 0002 says `models: string[]` is "models
|
// shape used by anthropic.mjs and codex.mjs (canonical-only, no alias spread).
|
||||||
// this provider serves" — an alias that resolves to a real model is itself
|
// D17 Finding 12 fix: alias resolution is handled in getProviderForModel() in
|
||||||
// served. Including both makes getProviderForModel() naively route either form:
|
// lib/providers/index.mjs (the SPOT for alias-aware routing), not by inflating
|
||||||
// model: "devstral-2-25-12" → mistral
|
// models[] with alias strings. This keeps models[] a pure canonical enumeration.
|
||||||
// model: "devstral-2" → mistral (via alias)
|
|
||||||
// model: "devstral" → mistral (via alias)
|
|
||||||
// Phase-2 fallback engine + dashboard will use canonical IDs internally.
|
|
||||||
const _registryEntry = modelsRegistryRaw?.providers?.mistral ?? {};
|
const _registryEntry = modelsRegistryRaw?.providers?.mistral ?? {};
|
||||||
const _registryModels = [
|
const _registryModels = (_registryEntry.models ?? []).map(m => m.id);
|
||||||
...(_registryEntry.models ?? []).map(m => m.id),
|
|
||||||
...Object.keys(_registryEntry.aliases ?? {}),
|
|
||||||
];
|
|
||||||
|
|
||||||
const mistral = {
|
const mistral = {
|
||||||
name: 'mistral',
|
name: 'mistral',
|
||||||
|
|||||||
+168
-22
@@ -533,6 +533,7 @@ describe('Plugin registry', () => {
|
|||||||
const r = getProviderForModel(m, 'alpha-v1');
|
const r = getProviderForModel(m, 'alpha-v1');
|
||||||
assert.ok(r !== null);
|
assert.ok(r !== null);
|
||||||
assert.equal(r.name, 'alpha');
|
assert.equal(r.name, 'alpha');
|
||||||
|
assert.equal(r.canonicalModel, 'alpha-v1', 'D17: canonicalModel must equal modelString on direct lookup');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getProviderForModel returns null for unknown model', () => {
|
it('getProviderForModel returns null for unknown model', () => {
|
||||||
@@ -600,6 +601,133 @@ function makeMockSpawn(stdoutChunks, exitCode = 0) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Suite D17: Alias-aware getProviderForModel (Finding 12 + 13) ─────────────
|
||||||
|
//
|
||||||
|
// Tests that getProviderForModel resolves aliases from models-registry.json
|
||||||
|
// to canonical IDs and routes to the correct (enabled) provider.
|
||||||
|
|
||||||
|
describe('D17 — alias-aware getProviderForModel', () => {
|
||||||
|
|
||||||
|
// ── Anthropic aliases ────────────────────────────────────────────────
|
||||||
|
it('D17: alias "sonnet" → anthropic, canonical claude-sonnet-4-6', () => {
|
||||||
|
const loaded = new Map([['anthropic', anthropic]]);
|
||||||
|
const r = getProviderForModel(loaded, 'sonnet');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'anthropic');
|
||||||
|
assert.equal(r.canonicalModel, 'claude-sonnet-4-6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: alias "claude" → anthropic, canonical claude-sonnet-4-6', () => {
|
||||||
|
const loaded = new Map([['anthropic', anthropic]]);
|
||||||
|
const r = getProviderForModel(loaded, 'claude');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'anthropic');
|
||||||
|
assert.equal(r.canonicalModel, 'claude-sonnet-4-6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: alias "opus" → anthropic, canonical claude-opus-4-7', () => {
|
||||||
|
const loaded = new Map([['anthropic', anthropic]]);
|
||||||
|
const r = getProviderForModel(loaded, 'opus');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'anthropic');
|
||||||
|
assert.equal(r.canonicalModel, 'claude-opus-4-7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: alias "haiku" → anthropic, canonical claude-haiku-4-5', () => {
|
||||||
|
const loaded = new Map([['anthropic', anthropic]]);
|
||||||
|
const r = getProviderForModel(loaded, 'haiku');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'anthropic');
|
||||||
|
assert.equal(r.canonicalModel, 'claude-haiku-4-5');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── OpenAI aliases ────────────────────────────────────────────────────
|
||||||
|
it('D17: alias "codex" → openai, canonical gpt-5.3-codex', () => {
|
||||||
|
const loaded = new Map([['openai', codex]]);
|
||||||
|
const r = getProviderForModel(loaded, 'codex');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'openai');
|
||||||
|
assert.equal(r.canonicalModel, 'gpt-5.3-codex');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: alias "gpt5" → openai, canonical gpt-5.5', () => {
|
||||||
|
const loaded = new Map([['openai', codex]]);
|
||||||
|
const r = getProviderForModel(loaded, 'gpt5');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'openai');
|
||||||
|
assert.equal(r.canonicalModel, 'gpt-5.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Mistral aliases ───────────────────────────────────────────────────
|
||||||
|
it('D17: alias "devstral" → mistral, canonical devstral-2-25-12', () => {
|
||||||
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
|
const r = getProviderForModel(loaded, 'devstral');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'mistral');
|
||||||
|
assert.equal(r.canonicalModel, 'devstral-2-25-12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: alias "devstral-small" → mistral, canonical devstral-small-2-25-12', () => {
|
||||||
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
|
const r = getProviderForModel(loaded, 'devstral-small');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'mistral');
|
||||||
|
assert.equal(r.canonicalModel, 'devstral-small-2-25-12');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Canonical pass-through ────────────────────────────────────────────
|
||||||
|
it('D17: canonical "claude-sonnet-4-6" → anthropic, canonicalModel unchanged', () => {
|
||||||
|
const loaded = new Map([['anthropic', anthropic]]);
|
||||||
|
const r = getProviderForModel(loaded, 'claude-sonnet-4-6');
|
||||||
|
assert.ok(r !== null);
|
||||||
|
assert.equal(r.name, 'anthropic');
|
||||||
|
assert.equal(r.canonicalModel, 'claude-sonnet-4-6');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Unknown model → null ──────────────────────────────────────────────
|
||||||
|
it('D17: unknown model "gpt-4-imaginary" → null', () => {
|
||||||
|
const loaded = new Map([['anthropic', anthropic], ['openai', codex], ['mistral', mistral]]);
|
||||||
|
assert.equal(getProviderForModel(loaded, 'gpt-4-imaginary'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Alias points to disabled provider → null ─────────────────────────
|
||||||
|
it('D17: alias "devstral" with only anthropic loaded → null (mistral not enabled)', () => {
|
||||||
|
// The alias is known (devstral → mistral) but mistral is not in loadedProviders.
|
||||||
|
const loaded = new Map([['anthropic', anthropic]]);
|
||||||
|
assert.equal(getProviderForModel(loaded, 'devstral'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: alias "sonnet" with only mistral loaded → null (anthropic not enabled)', () => {
|
||||||
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
|
assert.equal(getProviderForModel(loaded, 'sonnet'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── buildDefaultChain with alias ──────────────────────────────────────
|
||||||
|
it('D17: buildDefaultChain("sonnet") → chain carries canonical claude-sonnet-4-6', () => {
|
||||||
|
const loaded = new Map([['anthropic', anthropic]]);
|
||||||
|
const chain = buildDefaultChain('sonnet', loaded, {}, {});
|
||||||
|
assert.ok(chain !== null);
|
||||||
|
assert.equal(chain.length, 1);
|
||||||
|
assert.equal(chain[0].provider, 'anthropic');
|
||||||
|
assert.equal(chain[0].model, 'claude-sonnet-4-6');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: buildDefaultChain("devstral") → chain carries canonical devstral-2-25-12', () => {
|
||||||
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
|
const chain = buildDefaultChain('devstral', loaded, {}, {});
|
||||||
|
assert.ok(chain !== null);
|
||||||
|
assert.equal(chain.length, 1);
|
||||||
|
assert.equal(chain[0].provider, 'mistral');
|
||||||
|
assert.equal(chain[0].model, 'devstral-2-25-12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D17: buildDefaultChain("unknown-alias") with no providers → null', () => {
|
||||||
|
const loaded = new Map();
|
||||||
|
assert.equal(buildDefaultChain('unknown-alias', loaded, {}, {}), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('Anthropic plugin (D4)', () => {
|
describe('Anthropic plugin (D4)', () => {
|
||||||
|
|
||||||
// ── Test 1: Contract conformance ──────────────────────────────────────
|
// ── Test 1: Contract conformance ──────────────────────────────────────
|
||||||
@@ -2692,51 +2820,65 @@ describe('Mistral Vibe plugin (D8)', () => {
|
|||||||
assert.ok(mistral.displayName.toLowerCase().includes('mistral'));
|
assert.ok(mistral.displayName.toLowerCase().includes('mistral'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Test 4: models include all registry IDs + aliases ────────────────
|
// ── Test 4: models is canonical-only (D17 Finding 12 fix) ───────────
|
||||||
it('mistral.models includes every canonical registry id AND every alias', () => {
|
it('mistral.models contains only canonical registry IDs — no alias strings', () => {
|
||||||
// The plugin merges canonical IDs and alias keys into models[] so that
|
// D17 fix: models[] is canonical-only across all plugins. Alias routing
|
||||||
// getProviderForModel routes either form. Test that the registry's
|
// is the responsibility of getProviderForModel() in lib/providers/index.mjs.
|
||||||
// canonical IDs are a subset, and the aliases are all included too.
|
|
||||||
const registryIds = modelsRegistry.providers.mistral.models.map(m => m.id);
|
const registryIds = modelsRegistry.providers.mistral.models.map(m => m.id);
|
||||||
const registryAliases = Object.keys(modelsRegistry.providers.mistral.aliases ?? {});
|
const registryAliases = Object.keys(modelsRegistry.providers.mistral.aliases ?? {});
|
||||||
for (const id of registryIds) {
|
for (const id of registryIds) {
|
||||||
assert.ok(mistral.models.includes(id), `canonical id ${id} missing from mistral.models`);
|
assert.ok(mistral.models.includes(id), `canonical id ${id} missing from mistral.models`);
|
||||||
}
|
}
|
||||||
for (const alias of registryAliases) {
|
for (const alias of registryAliases) {
|
||||||
assert.ok(mistral.models.includes(alias), `alias ${alias} missing from mistral.models`);
|
assert.ok(!mistral.models.includes(alias), `alias ${alias} must NOT appear in mistral.models (D17)`);
|
||||||
}
|
}
|
||||||
assert.equal(mistral.models.length, registryIds.length + registryAliases.length);
|
assert.equal(mistral.models.length, registryIds.length, `Expected ${registryIds.length} canonical IDs, got ${mistral.models.length}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mistral.models contains canonical IDs + short-form aliases', () => {
|
it('mistral.models contains exactly the two canonical date-stamped IDs', () => {
|
||||||
// Per canonical models registry (docs.mistral.ai/getting-started/models/
|
// D17 fix: canonical-only shape. Length: 2 canonical IDs.
|
||||||
// models_overview, D8 review-2 finding): canonical IDs are date-stamped
|
|
||||||
// (devstral-2-25-12, devstral-small-2-25-12). Vibe config.toml uses short
|
|
||||||
// forms (devstral-2, devstral-small-2). Plugin exposes BOTH so
|
|
||||||
// getProviderForModel routes either form.
|
|
||||||
assert.ok(mistral.models.includes('devstral-2-25-12'), 'missing canonical devstral-2-25-12');
|
assert.ok(mistral.models.includes('devstral-2-25-12'), 'missing canonical devstral-2-25-12');
|
||||||
assert.ok(mistral.models.includes('devstral-small-2-25-12'), 'missing canonical devstral-small-2-25-12');
|
assert.ok(mistral.models.includes('devstral-small-2-25-12'), 'missing canonical devstral-small-2-25-12');
|
||||||
assert.ok(mistral.models.includes('devstral-2'), 'missing short-form alias devstral-2');
|
assert.ok(!mistral.models.includes('devstral-2'), 'alias devstral-2 must NOT be in models[] (D17)');
|
||||||
assert.ok(mistral.models.includes('devstral-small-2'), 'missing short-form alias devstral-small-2');
|
assert.ok(!mistral.models.includes('devstral-small-2'), 'alias devstral-small-2 must NOT be in models[] (D17)');
|
||||||
assert.ok(mistral.models.includes('devstral'), 'missing alias devstral');
|
assert.ok(!mistral.models.includes('devstral'), 'alias devstral must NOT be in models[] (D17)');
|
||||||
assert.ok(mistral.models.includes('devstral-small'), 'missing alias devstral-small');
|
assert.ok(!mistral.models.includes('devstral-small'), 'alias devstral-small must NOT be in models[] (D17)');
|
||||||
// Length: 2 canonical + 4 aliases = 6 total
|
// Length: 2 canonical only
|
||||||
assert.equal(mistral.models.length, 6, `Expected 6 entries (2 canonical + 4 aliases), got ${mistral.models.length}`);
|
assert.equal(mistral.models.length, 2, `Expected 2 canonical IDs, got ${mistral.models.length}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Test 5: getProviderForModel finds mistral for each model ──────────
|
// ── Test 5: getProviderForModel finds mistral for each model ──────────
|
||||||
it('getProviderForModel finds mistral for devstral-2', () => {
|
it('getProviderForModel finds mistral for canonical devstral-2-25-12', () => {
|
||||||
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
|
const result = getProviderForModel(loaded, 'devstral-2-25-12');
|
||||||
|
assert.ok(result !== null);
|
||||||
|
assert.equal(result.name, 'mistral');
|
||||||
|
assert.equal(result.canonicalModel, 'devstral-2-25-12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getProviderForModel finds mistral for alias devstral-2 → canonical devstral-2-25-12 (D17)', () => {
|
||||||
|
// D17: alias resolution now handled in getProviderForModel, not in models[].
|
||||||
const loaded = new Map([['mistral', mistral]]);
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
const result = getProviderForModel(loaded, 'devstral-2');
|
const result = getProviderForModel(loaded, 'devstral-2');
|
||||||
assert.ok(result !== null);
|
assert.ok(result !== null);
|
||||||
assert.equal(result.name, 'mistral');
|
assert.equal(result.name, 'mistral');
|
||||||
|
assert.equal(result.canonicalModel, 'devstral-2-25-12');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getProviderForModel finds mistral for devstral-small-2', () => {
|
it('getProviderForModel finds mistral for alias devstral → canonical devstral-2-25-12 (D17)', () => {
|
||||||
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
|
const result = getProviderForModel(loaded, 'devstral');
|
||||||
|
assert.ok(result !== null);
|
||||||
|
assert.equal(result.name, 'mistral');
|
||||||
|
assert.equal(result.canonicalModel, 'devstral-2-25-12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getProviderForModel finds mistral for alias devstral-small-2 → canonical devstral-small-2-25-12 (D17)', () => {
|
||||||
const loaded = new Map([['mistral', mistral]]);
|
const loaded = new Map([['mistral', mistral]]);
|
||||||
const result = getProviderForModel(loaded, 'devstral-small-2');
|
const result = getProviderForModel(loaded, 'devstral-small-2');
|
||||||
assert.ok(result !== null);
|
assert.ok(result !== null);
|
||||||
assert.equal(result.name, 'mistral');
|
assert.equal(result.name, 'mistral');
|
||||||
|
assert.equal(result.canonicalModel, 'devstral-small-2-25-12');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Test 6: irToMistral translation ──────────────────────────────────
|
// ── Test 6: irToMistral translation ──────────────────────────────────
|
||||||
@@ -3224,7 +3366,11 @@ describe('Mistral Vibe plugin (D8)', () => {
|
|||||||
const p = loaded.get('mistral');
|
const p = loaded.get('mistral');
|
||||||
const { valid, errors } = validateProvider(p);
|
const { valid, errors } = validateProvider(p);
|
||||||
assert.equal(valid, true, `Contract errors: ${errors.join('; ')}`);
|
assert.equal(valid, true, `Contract errors: ${errors.join('; ')}`);
|
||||||
assert.ok(p.models.includes('devstral-2'));
|
// D17: models[] is canonical-only. Verify via getProviderForModel instead of direct inclusion.
|
||||||
|
assert.ok(p.models.includes('devstral-2-25-12'), 'canonical devstral-2-25-12 must be in models[]');
|
||||||
|
const r = getProviderForModel(loaded, 'devstral-2');
|
||||||
|
assert.ok(r !== null, 'alias devstral-2 must route to mistral via getProviderForModel (D17)');
|
||||||
|
assert.equal(r.name, 'mistral');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Test 16: auth artifact reading helpers ────────────────────────────
|
// ── Test 16: auth artifact reading helpers ────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user