mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
cold-audit catch from 2026-05-24 (round 3)
3 round-3 P3 items batched per IDR. Mixed surfaces but all single-severity
and conceptually independent.
Changes (5 files, +324 / -17):
1. lib/ir/types.mjs — F8 validator extension (+28):
- `response_format`: must be object with string `.type` (undefined/omitted
accepted; null, non-object, missing-type all rejected). Forward-compat:
accepts any string for type so future OpenAI additions (json_schema,
etc.) flow through without schema bump.
- `tool_choice`: string form 'auto'/'none'/'required' OR object form
`{type:'function', function:{name:string}}`. All other shapes rejected.
Pre-D27 the validator silently accepted any value; the Anthropic plugin's
`if (irRequest.response_format?.type === 'json_object')` would no-op on
a malformed string payload.
2. docs/adr/0005-cache-cross-provider.md — F10 Amendment 4 (+8):
- Documents the IR-vs-body detection ambiguity in § D2: the ADR text
reads as if detection happens on the IR, but `openai-to-ir.mjs` strips
`cache_control` from messages per ADR 0003's whitelist policy, so
IR-side detection is structurally always empty at v1.0
- Documents the actual v1.0 detection mechanism (server.mjs side-channels
into the raw body)
- Documents the cache key `cache_control` slot's always-null status as
forward-compat (when a future ADR 0003 amendment adds cache_control
to IR, the slot will start carrying meaningful data without schema bump)
- Explicit "no code change" — F10 is docs-only
3. lib/providers/index.mjs — F15 alias map export (+11):
- `getAliasMap()` returns `new Map(_aliasMap)` — defensive copy preventing
caller mutation of the module-private alias map
- JSDoc documents use case + defensive-copy intent
4. server.mjs — F15 /v1/models alias surfacing (+25/-7):
- `handleModels` now emits TWO loops: canonical entries first (preserves
existing client expectations), then alias entries via `getAliasMap()`
- Each alias entry has the same 4 OpenAI-spec fields (id/object/created/
owned_by) — no invented fields per ALIGNMENT Rule 2(b)
- Disabled-provider alias non-leak: `loadedProviders.has(providerName)`
gate skips aliases whose target provider is not currently enabled
- createdTs reused (same per-request timestamp across all entries)
- JSDoc updated to document new ordering + F15 origin
5. test-features.mjs — +269 / +18 new tests:
- F8: 13 tests covering response_format (object/string/non-object/missing-
type) + tool_choice (string variants/object variants/wrong type/no name)
- F15: 5 tests covering canonical-first ordering, alias presence/count,
disabled-provider non-leak (with anthropic-only enabled, mistral and
openai aliases must NOT appear), Rule 2(b) shape conformance
Tests: 358 → 376 (+18). All pass on Node 20.
Reviewer notes (Iron Rule v1.6 § 10.x Mode A, fresh-context opus, independent
of drafter): APPROVE. Critical checks verified:
- F8: all 9+ tool_choice rejection axes traced through code (including
partial-function-object edge cases). The code handles them correctly even
where tests don't exercise (non-blocking gap).
- F10: amendment substantively correct. One wording-precision note: the
amendment says "body only" but the actual code is an OR-disjunction
(hasCacheControl(ir) || extractCacheControlMarkers(body.messages)).
Functionally identical at v1.0 because IR strips markers, so first
disjunct is always false. Forward-compat by construction.
- F15: disabled-provider non-leak verified by manual trace with
anthropic-only enabled — mistral/openai aliases correctly filtered out
by `loadedProviders.has(providerName)` gate. Test 17e explicitly
asserts this.
- F15+D17 round-trip: client GET /v1/models → sees alias entry → POSTs
with alias → getProviderForModel resolves via same _aliasMap → cache
key uses canonical → response works. Both surfaces read the same Map
(SPOT).
- F15+D23 cacheable interaction: cacheable opt-out doesn't suppress
alias surfacing in /v1/models — cacheable is about cache-write behavior
while discovery should still surface enabled providers. Intentional.
Authority:
- ADR 0003 § Optional fields (response_format + tool_choice IR shape)
- OpenAI Chat Completions API spec (the field semantics)
https://platform.openai.com/docs/api-reference/chat/create
- ADR 0005 § D2 + Amendment 4 (F10's own amendment landing here)
- ADR 0002 § Loading model + D17 getProviderForModel SPOT (F15's alias
origin)
- ALIGNMENT.md Rule 2(b) — no invented OpenAI fields
- CC 开发铁律 v1.6 § 10.x — Round-3 Cold Audit caught all 3
Follow-up items (reviewer's non-blocking suggestions, NOT in this PR):
- F8 micro-tests for null/boolean/partial-function inputs (code handles;
test gap only)
- F10 wording precision on OR-disjunction
- Nested-describe wrapper quirk in test-features.mjs (pre-existing
structural issue; D26/D27 describes are children of Suite 16 wrapper).
Cleanup in a future hygiene pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
174 lines
6.9 KiB
JavaScript
174 lines
6.9 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 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a defensive copy of the alias→{providerName, canonicalModel} map.
|
|
* Used by handleModels in server.mjs to emit alias entries in /v1/models.
|
|
* Defensive copy prevents caller mutation of the module-private _aliasMap.
|
|
*
|
|
* @returns {Map<string, { providerName: string, canonicalModel: string }>}
|
|
*/
|
|
export function getAliasMap() {
|
|
return new Map(_aliasMap);
|
|
}
|
|
|
|
// ── 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);
|
|
}
|