cold-audit catch from 2026-05-24 (round 3)
Three coordinated governance amendments per the major-decisions already
made (per "继续吧 unless major decision" + my F5/F11/F13+F14 option-(b)
calls earlier in the session). All P3-class, all docs-only. Single
PR per IDR cleanup-batch convention given the unified theme
("spec was aspirational; reality is X; amend spec to match").
Changes (3 docs files, +52 / -2):
1. docs/adr/0003-intermediate-representation.md — Amendment 1 (NEW
Amendments section, first ever for ADR 0003):
- F5 closure: removes the unimplemented `__irRoundTripTest()`
export-name claim from § Mitigations. The mental model
(symmetric irToNative+nativeToIR pair on each plugin) does NOT
fit the actual plugin shape (asymmetric spawn-wrappers:
request→CLI-args+stdin, response-stream→IR-chunks). Zero plugins
export this function.
- Documents the substitute test strategy that IS live at v0.1:
· Suite 3 covers entry-surface IR↔OpenAI round-trip
· Per-plugin spawn-with-mock test blocks cover IR→CLI→IR-chunk
· Lossy-translation edges documented inline in each plugin's
header comment (mistral.mjs DOCS-N markers being the most
thorough; anthropic.mjs + codex.mjs carry equivalent tables)
- Future plugins MUST satisfy both (a) spawn-with-mock test block
and (b) header lossy-field documentation — the enforcement
mechanism replacing the original export-name aspiration.
- Original § Mitigations bullet replaced with a pointer to
Amendment 1 (preserves history).
2. docs/adr/0005-cache-cross-provider.md — Amendment 5 (after D27
Amendment 4):
- F11 closure: acknowledges that the "Anthropic's own prompt cache
is consulted at the provider" half of § D2 is structurally
impossible at v0.1.
- Root cause: Anthropic plugin uses `claude -p --output-format text`
(verified at anthropic.mjs:196), a plain-text wire surface that
has no Messages-API field for cache_control markers. The markers
are also stripped at IR construction (openai-to-ir.mjs:45-89
whitelists fields, drops cache_control) per ADR 0003 IR design
reaffirmed in D27 Amendment 4 (F10).
- Clarifies v0.1 effective behavior: OLP-cache-bypass works
correctly (no double-caching when Anthropic + markers); the
delegate-to-Anthropic-prompt-cache half does not fire.
- Forward path (v1.x): switch Anthropic plugin to --output-format
json (NDJSON wire) or direct Messages API spawn — substantial
parser rewrite; deferred since no user has requested delegation.
- § D2 paragraph stays intact; an inline parenthetical reference
to Amendment 5 is added immediately after the affected sentence.
3. ALIGNMENT.md — new "Rule 4 exception class — Speculative-Candidate
Plugin" sub-section after Rule 5, before § Authorities:
- F13+F14 closure: formalizes the gap between strict ALIGNMENT
Rule 2 (no speculative shape assumptions) + Rule 4 (unalignable
plugins are deleted, not feature-flagged) and the actual practice
where Codex (D6) + Mistral (D8) plugins ship with explicit
UNPINNED assumptions documented in their headers, gated as
Candidate-not-Enabled.
- 5 precise conditions for a Speculative-Candidate plugin:
(1) STATIC_REGISTRY entry present, (2) candidate: true in
models-registry, (3) NOT in any user's providers.enabled config,
(4) header has explicit UNPINNED assumption section with labeled
IDs and planned-pin triggers, (5) defensive multi-shape parsers
tied to UNPINNED markers (greppable for future single-shape
replacement).
- Enablement criteria (Speculative-Candidate → Enabled): 3 explicit
requirements (remove UNPINNED labels from header; replace
multi-shape parsers with single-shape; file pin ADR amendment).
- Currently-in-class table:
· codex.mjs (D6) — A3 (auth token field name), A4 (NDJSON event
schema)
· mistral.mjs (D8) — A4 (JSON output event schema), A5 (model
flag), A6 (exact model IDs), A7 (streaming vs json output
mode), A8 (stdin prompt passing)
- Anthropic explicitly NOT in this class (CLI version pinned at
@anthropic-ai/claude-code v2.1.89 per Provider Authority Pins
table).
- CI enforcement noted as deferred (reviewer obligation today; a
future PR may add grep-based gate).
Pre-commit fold-in (per evidence-first checkpoint #4):
- **D31 reviewer flagged wording precision** ("Provider Inventory
table above" was structurally wrong — the table is BELOW the new
sub-section). Folded in: 1-word fix "above" → "below" on the
Speculative-Candidate condition #2.
Tests: 400/400 unchanged (docs-only).
Authority:
- ADR 0003 § Mitigations (modified) + new Amendment 1 (self-amendment)
- ADR 0005 § D2 (annotated) + new Amendment 5 (self-amendment)
- ALIGNMENT.md § Rules (extended with Rule 4 exception class)
- D27 F10 Amendment 4 of ADR 0005 (cited for cache_control IR-strip
precedent)
- CC 开发铁律 v1.6 § 10.x — Round-3 Cold Audit caught all 3 items
Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus, independent
of drafter): APPROVE_WITH_MINOR. Verified:
- Suite 3 + 3 plugin spawn-with-mock blocks + 3 plugin lossy-field
header tables all present (F5 substitute test strategy claims
match implementation)
- anthropic.mjs:196 hardcodes --output-format text + openai-to-ir.mjs
drops cache_control from messages (F11 wire-limitation technically
accurate)
- All 8 UNPINNED assumption labels (codex A3/A4 + mistral A4/A5/A6/A7/A8)
match plugin header text exactly — no invention
- Anthropic-not-in-class claim verified against Provider Authority Pins
table
Follow-up items (reviewer's non-blocking notes, NOT in this PR):
1. **mistral.mjs A5 internal disconnect** — header lines 145-156 label
A5 as `UNPINNED-D-later-verifies`, but function body at 371-374
already cites DeepWiki enumeration as the pin and marks A5
`CONFIRMED-NOT-APPLICABLE` (no --model flag exists in vibe CLI).
The disconnect predates D31 and is a per-plugin header cleanup
(not a constitutional amendment), so it stays out of D31 scope per
IDR. File as separate follow-up issue — A5 should be moved out of
the Speculative-Candidate table once the header is updated.
2. **ADR 0005 Amendment ordering cosmetic** — strict reverse-chronological
would put Amendment 5 above Amendment 4 (current order is 4, 5, 3,
2 because both 4 and 5 share the 2026-05-24 date and 5 was inserted
after 4). Not blocking; future cleanup if maintainers want strict
numeric-descending.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
14 KiB
ADR 0003 — Intermediate Representation (IR) Design
- Date: 2026-05-23
- Status: Accepted (bootstrap)
- Authors: project maintainer (with AI drafting assistance)
- Related: OLP v0.1 spec §4.1, §4.2 (IR subsection); ADR 0002 (plugin architecture); ADR 0004 (fallback engine — requires shape-stable requests for safe replay)
Amendments
Amendment 1 — 2026-05-24: Remove __irRoundTripTest() claim; document substitute test strategy (D31 F5)
- Finding: Round-3 cold-audit F5 (P2 ADR-claim vs implementation drift) — § Mitigations names a
__irRoundTripTest()export on every provider plugin as the structural counter-measure against silent lossy translation. ZERO plugins export this function; the export-name was an early-design aspiration that does not fit the actual plugin shape (spawn-wrappers with asymmetric request→CLI-args+stdin and response-stream→IR, not symmetricirToNative+nativeToIRfunctions that could be tested in isolation via a round-trip fixture). - Decision: Remove the
__irRoundTripTest()export-name claim from § Mitigations. The structural counter-measure against silent lossy translation is preserved via the existing test strategy documented below; the goal (CI-visible lossy-translation coverage) is met; only the mechanism name was wrong. - Substitute test strategy (live at v0.1):
- Entry-surface IR↔OpenAI round-trip — Suite 3 (
irChunkToOpenAISSE format+openai-to-irtests intest-features.mjs) covers the OpenAI ↔ IR direction at the entry surface. - Per-plugin spawn-with-mock tests in each plugin's test block (anthropic, codex, and mistral spawn unit tests) exercise the IR → CLI args → IR-chunk round-trip end-to-end.
- Lossy-translation edges are documented inline in each plugin's header comment;
mistral.mjs's explicit "DOCS-N" UNPINNED markers are the most thorough example;anthropic.mjsandcodex.mjscarry equivalent lossy-field tables in their headers. - Future plugins MUST include both: (a) a spawn-with-mock test block, and (b) header documentation of any lossy IR fields. These two requirements are the enforcement mechanism replacing the
__irRoundTripTest()export-name aspiration.
- Entry-surface IR↔OpenAI round-trip — Suite 3 (
- Original § Mitigations update: The
__irRoundTripTest()sentence in § Mitigations below is replaced with a pointer to this amendment. See § Mitigations. - Procedural mechanism: CC 开发铁律 v1.6 § 10.x (Round-3 Cold Audit caught it as F5).
Context
OLP exposes exactly one external API surface: OpenAI-compatible /v1/chat/completions (per spec §4.1). Internally, OLP fans out to N providers, each with its own native protocol — Anthropic Messages API, OpenAI Chat Completions, Mistral La Plateforme, xAI Grok, Moonshot Kimi, etc. The combinatorial fact is unavoidable: a request entering as OpenAI shape must exit as Anthropic shape (or Mistral shape, or whatever the routing chain advances to), and the response on the way back must travel the inverse path.
The ad-hoc approach is per-provider entry-to-native translation:
provider.spawn(openAIRequest) {
const native = openAIToProviderNative(openAIRequest); // per-provider impl
...
const openAIResponse = providerNativeToOpenAI(nativeResponse); // per-provider impl
return openAIResponse;
}
At three providers, that's six translation functions (three forward + three reverse). At eight providers it's sixteen. None of them share code — Anthropic-to-OpenAI and Mistral-to-OpenAI have different shape mismatches, different tool-calling translations, different streaming-event mappings. The "OpenAI-shape canonical" assumption privileges one provider's protocol as the lingua franca, which is wrong: OpenAI's Chat Completions is OLP's entry surface, not OLP's internal representation. Anchoring internal logic on the entry surface means every entry-surface change (a new OpenAI field, a deprecated OpenAI field, a new OpenAI streaming-event variant) ripples through every provider plugin.
The structural answer is an Intermediate Representation (IR): a canonical OLP-internal shape that all provider plugins consume (forward) and produce (reverse). The entry adapter translates OpenAI → IR once. Each provider plugin translates IR → native and native → IR. The combinatorial cost goes from N² to 2N: one entry adapter + N provider translators.
The IR is not OpenAI Chat Completions, and it is not Anthropic Messages. It is a deliberately-OLP-owned shape, normalized to be expressible in every provider's native shape with documented lossy fields where a provider's expressiveness is narrower than the IR. Choosing IR shape carefully — neither so rich it cannot round-trip through Mistral, nor so narrow it loses Anthropic's tool_use semantics — is the load-bearing design decision of this ADR.
The fallback engine (ADR 0004) is a second consumer of IR's shape stability. When a request fails over from Anthropic to OpenAI, the IR that was constructed at the entry point is replayed against the next provider. If the IR were a thin OpenAI wrapper, replay against OpenAI would be trivial but replay against Mistral or xAI would re-run the same shape-translation pain. A properly-OLP-owned IR makes replay symmetric across providers.
Decision
OLP defines an Intermediate Representation (IR) as the canonical internal shape between the OpenAI-compat entry surface and each provider plugin. Per spec §4.2 (IR subsection), the IR v1.0 encodes:
Required fields:
messages[]— each withrole,content, optionalname, optionaltool_calls, optionaltool_call_id. Roles supported:system,user,assistant,tool.model— the user-requested model string. The provider plugin maps this to the provider-native model identifier (e.g.,claude-sonnet-4-6→claude-sonnet-4-6-20260301for Anthropic).stream— boolean. SSE expected on the entry side iff true.
Optional fields:
max_tokens— integer; provider plugins clamp to provider-supported maxima.temperature— float [0, 2]; provider plugins map to provider-native range (e.g., Anthropic uses [0, 1]).top_p— float [0, 1].stop— string or string[]; provider plugins handle the single-vs-array shape difference per native protocol.tools[]+tool_choice— function-calling subset. v1.0 supports basic function definitions (name,description,parametersJSON-schema); advanced features (parallel tool use, tool result handling beyond single round-trip) are per-provider documented as caveats per spec §8 Q-C.response_format— best-effort; provider-specific (e.g., Anthropic does not natively honor aresponse_format: json_objectfield, so this becomes a system-prompt augmentation on that provider).
Translation direction model. Each provider plugin owns two translation functions:
// IR → provider-native shape
const nativeRequest = provider.irToNative(irRequest);
// provider-native response chunk → IR response chunk
const irChunk = provider.nativeToIR(nativeChunk);
The entry adapter (server.mjs's /v1/chat/completions handler) owns the inverse:
const irRequest = openAIToIR(openAIRequest);
const irResponseStream = await provider.spawn(irRequest, authContext);
for await (const irChunk of irResponseStream) {
res.write(irToOpenAI(irChunk));
}
Lossy-translation documentation requirement. When a provider plugin's irToNative or nativeToIR cannot losslessly carry an IR field (e.g., Mistral Vibe does not support tool_choice: "required"), the lossy edge is documented in the provider plugin's header comment AND in docs/provider-caveats.md (📋 planned — not yet authored; until it exists, lossy translations are recorded only in the provider plugin header comments). The caveats document is referenced from /v1/models response metadata so clients can see provider-specific limits without reading source.
IR is versioned. This ADR ratifies IR v1.0. Future additions (e.g., a reasoning_effort field if reasoning-model behavior diverges enough across providers to warrant explicit modeling) require an amendment ADR. Removals are breaking changes and require a major-version bump of OLP.
The IR is not exposed externally. It is OLP-internal. There is no /v1/ir/* endpoint, no client SDK in IR shape, no documented external IR contract. The only public contract is OpenAI /v1/chat/completions; the IR is an implementation detail that the maintainer (and the maintainer's reviewers) reason about, not a contract with clients.
Consequences
Positive
- Adding a provider is N translation functions (one IR→native, one native→IR), not 2N (one openai→native, one native→openai for every provider). The maintenance burden of "OpenAI shipped a new field" is one entry-adapter change, not one change per provider.
- The fallback engine (ADR 0004) replays the same IR object across providers in a chain. Replay correctness depends on IR being a fixed-shape data structure; there is no per-provider state leaking through that the next chain hop has to re-derive.
- Lossy translations are documented per-provider and surfaced to clients via
docs/provider-caveats.md(📋 planned) +/v1/modelsmetadata. A client choosing a fallback chain can see "this chain's third hop is Mistral, which does not support tool_choice=required" before they hit the failure mode. - The IR is OLP's source of truth for "what was actually requested" for cache-key generation (ADR 0005). Caching on OpenAI shape would couple cache keys to OpenAI field naming; caching on IR makes the cache key provider-agnostic.
Negative
- Two translation layers per request: openai→IR at entry, IR→native at provider. That's two passes of object construction per request, plus the inverse on the way back. For family-scale traffic this is unmeasurable; at higher scale the overhead would need profiling.
- IR design decisions are sticky. Choosing the wrong shape for
tool_callsin v1.0 (e.g., privileging OpenAI's parallel-tool-call shape over Anthropic's single-tool shape) would force every provider plugin to work around the bad choice forever. IR v1.0 is intentionally narrow (basic function calling only) to give the maintainer space to expand the IR with informed amendments rather than commit too early. - The IR is a third concept (alongside OpenAI shape and per-provider native shape) that every contributor must internalize. Onboarding a new contributor means teaching three shapes, not two. The PR template and OLP ALIGNMENT.md call out the IR explicitly so contributors know which shape applies in which layer of code.
Mitigations
- IR v1.0 is deliberately a subset, not a superset, of what providers can do. Where providers diverge in expressive power (tool calling, structured outputs, reasoning modes), the IR encodes the common subset in v1.0, with caveats documenting provider extensions. Expanding the IR to cover provider extensions is an explicit amendment, not a silent generalization.
- (The
__irRoundTripTest()export-name claim that originally appeared here was removed by Amendment 1 (D31 F5). The structural counter-measure against silent lossy translation is the substitute test strategy described in Amendment 1: entry-surface round-trip tests in Suite 3 oftest-features.mjs+ per-plugin spawn-with-mock test blocks + inline lossy-field header documentation. Future plugins must satisfy both requirements.) - The IR has no external surface. If IR v1.0 turns out to be the wrong shape and IR v2.0 is incompatible, the migration is OLP-internal: provider plugins are updated, the entry adapter is updated, no client code changes. The cost of being wrong is bounded by the project's own codebase.
Alternatives considered
(a) Full pass-through with no normalization — OpenAI request goes straight to each provider plugin. Provider plugins each implement their own openai→native + native→openai. Rejected: this is the N² shape the IR exists to avoid. Worse, it makes the fallback engine (ADR 0004) harder, because each fallback hop has to redo openai→native; there is no canonical "what was requested" object to replay. The shape-stability invariant the fallback engine depends on becomes per-provider rather than systemic.
(b) Per-provider entry endpoints — /v1/anthropic/messages, /v1/openai/chat/completions, /v1/mistral/.... Each entry endpoint speaks its provider's native shape; no translation. Rejected: this just relocates the N² problem to the client side — each client now needs N integrations (one per provider's wire format), and the unified-endpoint value proposition (spec §1: "one HTTP endpoint, multiple subscriptions behind it") is dead on arrival. Cross-provider fallback also stops being client-transparent: clients have to know about the fallback because they have to switch endpoints when the primary fails.
(c) Use Anthropic Messages format as the IR (Anthropic-native as canonical). This privileges Anthropic's shape as OLP's internal language. Rejected on two grounds: (1) Anthropic is currently one provider in eight; privileging its shape codifies provider preference into the IR, which is the wrong abstraction layer; (2) Anthropic-shape is rich enough to be lossy when translated to spartans like Mistral Vibe, so the IR would have features no other provider could use without elaborate fallback in the translator. The IR should be neutral, with provider extensions documented per-plugin, not Anthropic-shaped with per-plugin de-features.
(d) Use OpenAI Chat Completions as the IR (entry-surface as canonical). Privileges OpenAI shape. Rejected: this is the implicit shape of alternative (a), just lifted into a named layer. The coupling to OpenAI's evolving spec means every OpenAI deprecation / addition is a cross-cutting change. The IR exists precisely to decouple OLP-internal logic from OpenAI's release cadence.
(e) Defer IR design and start with two providers tightly coupled (Anthropic + OpenAI), refactor to IR when adding the third. Rejected: this defers the architectural decision into a code-rewrite-under-pressure moment (adding Mistral while two providers are in production). The pattern of "we'll abstract it later" is what produced the hybrid awkwardness OCP ADR 0005 warned against. Pay the IR cost up front, with v1.0 deliberately narrow, and expand via amendment.
Sources
- OLP v0.1 spec §4.1 (Single-protocol entry: OpenAI-compatible)
- OLP v0.1 spec §4.2 (Plugin-based provider system, including IR subsection)
- OLP v0.1 spec §8 Q-C (Tool-calling translation differences across providers — informs why IR v1.0 keeps tool calling to a basic subset)
- OCP ADR 0003 (
models.jsonSPOT) — informs the "OLP-owned canonical shape, not external surface" framing