mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
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>
163 lines
6.5 KiB
JavaScript
163 lines
6.5 KiB
JavaScript
/**
|
|
* lib/providers/index.mjs — Static provider registry
|
|
*
|
|
* Authority: ADR 0002 § "Loading model"
|
|
*
|
|
* This file is a hand-maintained static enumeration. There is no filesystem
|
|
* scan, no dynamic discovery, no npm-installed plugin loading. Adding a
|
|
* provider requires: (1) write lib/providers/<name>.mjs, (2) add one import +
|
|
* one entry to STATIC_REGISTRY below, (3) README § "Supported Providers",
|
|
* (4) inclusion ADR per ADR 0006.
|
|
*
|
|
* At D4 (Phase 1 Day 2), the anthropic plugin is added to STATIC_REGISTRY
|
|
* as a Candidate. It is NOT Enabled by default — a config with
|
|
* { enabled: { anthropic: true } } is required, which only happens after D5
|
|
* E2E audit passes per ALIGNMENT.md § Provider Inventory.
|
|
*
|
|
* At D6 (Phase 1 Day 4), the openai (Codex) plugin is added to STATIC_REGISTRY
|
|
* as a Candidate. Enabled by { enabled: { openai: true } } after D7 E2E audit.
|
|
*
|
|
* At D8 (Phase 1 Day 5), the mistral (Mistral Vibe) plugin is added to
|
|
* STATIC_REGISTRY as a Candidate. Enabled by { enabled: { mistral: true } }
|
|
* after D-later E2E audit. Authority: ADR 0006 Tier D classification for
|
|
* Mistral Vibe (Le Chat Pro); docs-based authority from
|
|
* 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 anthropicDefault from './anthropic.mjs';
|
|
import codexDefault from './codex.mjs';
|
|
import mistralDefault from './mistral.mjs';
|
|
import modelsRegistryRaw from '../../models-registry.json' with { type: 'json' };
|
|
|
|
// Normalize default export pattern
|
|
const anthropic = anthropicDefault;
|
|
const codex = codexDefault;
|
|
const mistral = mistralDefault;
|
|
|
|
// ── Static registry ───────────────────────────────────────────────────────
|
|
|
|
const STATIC_REGISTRY = [
|
|
anthropic,
|
|
codex,
|
|
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 ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Loads and validates providers, filtering by the enabled set in config.
|
|
* At D4, the anthropic provider is Candidate only — it will not appear in
|
|
* the loaded Map unless config.enabled.anthropic === true (set by D5 after E2E).
|
|
*
|
|
* @param {{ enabled?: Record<string,boolean> }} [config={}]
|
|
* @returns {Map<string, import('./base.mjs').ProviderContractV1>}
|
|
*/
|
|
export function loadProviders(config = {}) {
|
|
const loaded = new Map();
|
|
|
|
for (const p of STATIC_REGISTRY) {
|
|
const { valid, errors } = validateProvider(p);
|
|
if (!valid) {
|
|
throw new Error(`Provider ${p?.name ?? 'unknown'} fails contract validation: ${errors.join('; ')}`);
|
|
}
|
|
|
|
const enabled = config.enabled?.[p.name] === true;
|
|
if (enabled) {
|
|
loaded.set(p.name, p);
|
|
}
|
|
}
|
|
|
|
return loaded;
|
|
}
|
|
|
|
/**
|
|
* Returns the provider in `loadedProviders` that serves `modelString`, with
|
|
* 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.
|
|
*
|
|
* Resolution order:
|
|
* 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 {string} modelString
|
|
* @returns {{ provider: import('./base.mjs').ProviderContractV1, name: string, canonicalModel: string }|null}
|
|
*/
|
|
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) {
|
|
if (p.models.includes(modelString)) {
|
|
return { provider: p, name, canonicalModel: modelString };
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Looks up a provider by name in the loaded Map. Returns null if not found.
|
|
* Useful for tests that need to access a specific provider directly.
|
|
*
|
|
* @param {Map<string, import('./base.mjs').ProviderContractV1>} loadedProviders
|
|
* @param {string} name
|
|
* @returns {import('./base.mjs').ProviderContractV1|null}
|
|
*/
|
|
export function getProviderByName(loadedProviders, name) {
|
|
return loadedProviders.get(name) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Returns all provider names in the static registry (whether enabled or not).
|
|
* Used by /health and diagnostics.
|
|
* At D4: returns ['anthropic']; at D6: returns ['anthropic', 'openai'];
|
|
* at D8: returns ['anthropic', 'openai', 'mistral'].
|
|
*
|
|
* @returns {string[]}
|
|
*/
|
|
export function listAllProviderNames() {
|
|
return STATIC_REGISTRY.map(p => p.name);
|
|
}
|