mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
feat: D27 — round-3 P3 batch (F8 IR validator + F10 ADR amend + F15 /v1/models aliases)
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>
This commit is contained in:
+21
-4
@@ -29,7 +29,7 @@ import {
|
||||
generateRequestId,
|
||||
SSE_DONE,
|
||||
} from './lib/ir/ir-to-openai.mjs';
|
||||
import { loadProviders, listAllProviderNames } from './lib/providers/index.mjs';
|
||||
import { loadProviders, listAllProviderNames, getAliasMap } from './lib/providers/index.mjs';
|
||||
import { ProviderError } from './lib/providers/base.mjs';
|
||||
import { computeCacheKey, hasCacheControl, extractCacheControlMarkers } from './lib/cache/keys.mjs';
|
||||
import { CacheStore } from './lib/cache/store.mjs';
|
||||
@@ -259,17 +259,21 @@ function handleHealth(req, res) {
|
||||
* Returns the list of models served by all currently loaded (enabled) providers.
|
||||
* Per ADR 0002 § "Loading model" + OpenAI spec /v1/models:
|
||||
* Each entry: { id, object: 'model', created, owned_by }
|
||||
* - id: canonical model ID from the provider's models[] array
|
||||
* - id: canonical model ID (or alias string for alias entries)
|
||||
* - object: literal 'model' (OpenAI spec)
|
||||
* - created: Unix epoch seconds (stable per request; computed once from Date.now())
|
||||
* - owned_by: provider.name (e.g. 'anthropic', 'openai', 'mistral')
|
||||
* Only canonical IDs are emitted (no aliases — per D17 SPOT decision).
|
||||
* Order: insertion order of loadedProviders, then insertion order of each provider's models[].
|
||||
* Order: canonical models first (insertion order of loadedProviders, then models[]),
|
||||
* then alias entries (for aliases whose target provider is currently loaded).
|
||||
* Authority: OpenAI /v1/models spec (https://platform.openai.com/docs/api-reference/models);
|
||||
* models-registry.json alias map (SPOT per D17); F15 round-3 adds alias surfacing.
|
||||
* Empty case: if no providers are enabled, data: [] is returned naturally.
|
||||
*/
|
||||
function handleModels(req, res) {
|
||||
const createdTs = Math.floor(Date.now() / 1000);
|
||||
const data = [];
|
||||
|
||||
// Canonical entries first
|
||||
for (const [providerName, provider] of loadedProviders) {
|
||||
for (const modelId of provider.models) {
|
||||
data.push({
|
||||
@@ -280,6 +284,19 @@ function handleModels(req, res) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Alias entries for loaded (enabled) providers, canonical-first ordering preserved
|
||||
for (const [alias, { providerName }] of getAliasMap()) {
|
||||
if (loadedProviders.has(providerName)) {
|
||||
data.push({
|
||||
id: alias,
|
||||
object: 'model',
|
||||
created: createdTs,
|
||||
owned_by: providerName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendJSON(res, 200, { object: 'list', data });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user