docs+fix+test: D36 — pre-Phase-2 batch #2 (issues #2 #5 #6 #13 #14 #15)

Second batch of pre-Phase-2 cleanup. 6 GitHub issues closed in one
cohesive docs/governance commit with 1 small code addition (#2 debug
log line in server.mjs) and 1 transcript artifact (#15 new file).

Changes (7 files modified, 1 file created, +470 / -13):

**Code changes**

1. **#2 — cache_control partial-noop debug log** (server.mjs)

   ADR 0005 § D2 says: "for non-Anthropic targets, the bypass markers
   are noop'd (logged once per request at debug level so users can
   see they were ignored)". Pre-D36 logEvent fired only when an
   Anthropic hop actually bypassed; non-Anthropic hops with markers
   present were silently noop'd.

   New 12-line block in handleChatCompletions right after
   hasCacheControlMarkers is computed. Fires logEvent('debug',
   'cache_control_partial_noop', { chain: [<provider names>],
   marker_count: <count> }) when:
   - hasCacheControlMarkers === true AND
   - chain.some(hop => hop.provider !== 'anthropic')

   Fires at most once per request (top-level if, not in a loop). No
   log when no markers, or when every chain hop is Anthropic. The
   existing cache_bypass debug log inside shouldBypassCacheForHop is
   untouched.

   marker_count sums body-side and IR-side extractCacheControlMarkers
   results. At v0.1 the IR term is structurally 0 (openAIToIR strips
   cache_control); inline comment marks this as a revisit point for
   the future ADR 0003 amendment that activates cache_control in the
   IR whitelist.

**Documentation amendments**

2. **#5 — ADR 0002 vibe.mjs → mistral.mjs** (docs/adr/0002-plugin-architecture.md)

   § Decision filesystem layout: `vibe.mjs` corrected to
   `mistral.mjs` (file named after provider key per the convention
   anthropic.mjs/codex.mjs). The vibe.mjs entry was an early-draft
   naming choice that never landed. Amendment 5 prepended above
   Amendment 4 documenting the filename correction + explicit
   convention statement (file named after provider key, not CLI
   binary) for future contributors.

3. **#6 — mistral.mjs A5 flip + ALIGNMENT.md table update**
   (lib/providers/mistral.mjs + ALIGNMENT.md)

   Pre-D36: header A5 (model flag) status was UNPINNED-D-later-verifies
   but function body lines 376-380 said CONFIRMED-NOT-APPLICABLE
   (DeepWiki enumeration already confirmed `--model` does not exist
   on vibe CLI). Header now reflects CONFIRMED-NOT-APPLICABLE with
   DeepWiki citation (DOCS-4). ALIGNMENT.md Speculative-Candidate
   table mistral row: A5 removed from UNPINNED list; A4, A6, A7, A8
   preserved with parenthetical descriptions intact. No code change
   to function body (already correct).

4. **#13 — /v1/models alias entries — ALIGNMENT.md + spec-pin
   governance** (ALIGNMENT.md + docs/openai-spec-pin.md)

   Round-6 F10 flagged D27 F15's alias entries on /v1/models as
   borderline Rule 2(b) violation (OpenAI spec does not enumerate
   aliases as separate entries). Option C selected: keep current
   behavior, document the controlled deviation.

   - ALIGNMENT.md: new "Controlled deviations (entry-surface scope)"
     subsection under "Class-specific Exceptions". Entry 1 documents
     the /v1/models alias deviation with rationale (D27 F15
     onboarding), formal contract reference, field constraints
     (owned_by matches canonical, created matches canonical, no
     invented fields), SPOT reference (getAliasMap()), and re-
     evaluation trigger.
   - docs/openai-spec-pin.md: new alias-surfacing subsection under
     GET /v1/models with full 4-field contract table (id/object/
     created/owned_by), rationale, sourcing explanation, forward
     path.
   - server.mjs handleModels: NO CHANGE — behavior preserved.

5. **#15 — Anthropic v2.1.89 transcript artifact**
   (docs/provider-audits/anthropic.md NEW; ALIGNMENT.md +
   lib/providers/anthropic.mjs cross-references)

   Round-6 F12: ALIGNMENT.md anthropic row pin (v2.1.89, observed at
   D4) cited the plugin header; plugin header cited the observation
   date but no transcript. Circular per Rule 1 ("observed behaviour,
   transcript attached").

   New file docs/provider-audits/anthropic.md (single living artifact,
   not version-specific):
   - Date of capture: 2026-05-24
   - Observed `claude --version`: 2.1.132 (Claude Code) — captured
     today on the project maintainer's primary workstation
   - Plugin-pinned version: @anthropic-ai/claude-code v2.1.89 (from
     D4 implementation pin in ALIGNMENT.md Provider Authority Pins)
   - Version drift: honestly documented — pin is v2.1.89, live is
     v2.1.132, drift within tolerance, re-audit triggers named
   - Sample invocation: `claude -p --output-format text --no-session-
     persistence --model <model> [--debug]`
   - Flag-surface table: 5 OLP-consumed flags verbatim from
     `claude -p --help` (-p / --output-format / --no-session-
     persistence / --model / --debug)
   - Citation cross-references back to ALIGNMENT.md + plugin header

   ALIGNMENT.md anthropic row: appended "transcript artifact: docs/
   provider-audits/anthropic.md (captured 2026-05-24)" — closes the
   circular citation.

   lib/providers/anthropic.mjs header: 5-line pointer to the artifact
   with version numbers stated explicitly.

**Tests** (test-features.mjs): 424 → 431 (+7):

- #2 partial-noop log ×3:
  - Suite 9f case 1: markers + mixed chain → fires once at level=debug
  - Suite 9f case 2: no markers → suppressed
  - Suite 9f case 3: anthropic-only chain → suppressed

- #14 cache_control slot determinism ×4:
  - #14a: markers-present IR produces different key from no-markers IR
  - #14b: same IR computed twice yields identical key
  - #14c: two independently-constructed IRs with identical payloads
    yield same key
  - #14d: both top-level and content-array-nested markers affect the key

  All 4 tests call computeCacheKey directly on hand-built IRs
  (bypassing openAIToIR which strips markers at v0.1). Per ALIGNMENT.md
  Rule 2 (No Invention), no sortMarkers helper added — the slot is
  dead-code at v0.1 and shipping a helper without a caller authority
  would be invention. Test comment documents the forward-activation
  contract.

Pre-commit fold-in (per evidence-first checkpoint #4):
- **D36 reviewer flagged marker_count latent double-count risk**
  (Suggestion #1, non-blocking). The sum at server.mjs:435-437 is
  safe at v0.1 (IR term structurally 0) but will 2× when a future
  ADR 0003 amendment activates cache_control in the IR whitelist.
  Folded in a 4-line comment marking the revisit point.

Two other non-blocking reviewer suggestions not folded:
- Test count brief-vs-deliverable discrepancy (4 not 3 for #14) is
  informational — the 4-test variant is strictly better (covers
  content-array nesting which is a real extractCacheControlMarkers
  contract path).
- Recapture-procedure git-add reminder in anthropic.md is low-priority
  procedure documentation.

Authority:
- ADR 0005 § D2 — cache_control partial-noop debug log requirement (#2)
- ADR 0002 § Decision filesystem layout (Amendment 5) — plugin file
  naming convention (#5)
- DeepWiki vibe CLI flag enumeration — A5 not applicable (#6)
- ALIGNMENT.md Rule 2(b) + docs/openai-spec-pin.md GET /v1/models —
  alias controlled deviation (#13)
- ADR 0005 cache key stability invariant + ADR 0003 forward-compat —
  cache_control slot determinism contract (#14)
- ALIGNMENT.md Rule 5 (observed behaviour, transcript attached) +
  Provider Authority Pins anthropic row — transcript artifact (#15)
- CC 开发铁律 v1.6 § 10.x — independent fresh-context reviewer
- CLAUDE.md release_kit_overlay phase_rolling_mode — D36 under
  "Unreleased" against Phase 2; no version bump

Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus,
independent of drafter): APPROVE. Critical depth checks:
- #2: gate condition fires only on (markers AND non-anthropic-hop);
  pre-existing cache_bypass log inside shouldBypassCacheForHop
  untouched
- #5: lib/providers/ directory verified — mistral.mjs exists,
  vibe.mjs does not exist
- #6: mistral.mjs spawn-site body comment (lines 376-380) already
  CONFIRMED-NOT-APPLICABLE pre-D36 and unchanged in this diff
- #13: server.mjs handleModels unchanged (verified via grep)
- #14: all 4 tests pass against current code; no sortMarkers helper
  shipped (Rule 2 No Invention honored)
- #15: live `claude --version` independently run by reviewer →
  matches artifact (2.1.132); all 5 OLP-consumed flags independently
  verified present in `claude -p --help`
- Hygiene: 0 hits for personal markers, home paths, OAuth tokens,
  internal IPs across all 8 files
- 431/431 tests pass in reviewer's independent npm test run

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 20:38:16 +10:00
co-authored by Claude Opus 4.7
parent 2600185edb
commit e96752a528
8 changed files with 591 additions and 13 deletions
+12 -2
View File
@@ -56,7 +56,7 @@ A plugin satisfying all five conditions is a **Speculative-Candidate**. It is Ru
| Plugin file | Phase | Labelled UNPINNED assumptions | | Plugin file | Phase | Labelled UNPINNED assumptions |
|---|---|---| |---|---|---|
| `lib/providers/codex.mjs` (D6) | Phase 2 | A3 (auth token field name), A4 (NDJSON event schema) | | `lib/providers/codex.mjs` (D6) | Phase 2 | A3 (auth token field name), A4 (NDJSON event schema) |
| `lib/providers/mistral.mjs` (D8) | Phase 3 | A4 (JSON output event schema), A5 (model flag), A6 (exact model IDs), A7 (streaming vs json output mode), A8 (stdin prompt passing) | | `lib/providers/mistral.mjs` (D8) | Phase 3 | A4 (JSON output event schema), A6 (exact model IDs), A7 (streaming vs json output mode), A8 (stdin prompt passing) |
The Anthropic plugin (`lib/providers/anthropic.mjs`, D4D5) is NOT in this class — its CLI authority is pinned (`@anthropic-ai/claude-code` v2.1.89 per the Provider Authority Pins table above). It conforms to the standard Rule 4 path. The Anthropic plugin (`lib/providers/anthropic.mjs`, D4D5) is NOT in this class — its CLI authority is pinned (`@anthropic-ai/claude-code` v2.1.89 per the Provider Authority Pins table above). It conforms to the standard Rule 4 path.
@@ -76,7 +76,7 @@ Each provider plugin in `lib/providers/<name>.mjs` is governed by the underlying
| Provider key | Provider CLI | Audit pin (TBD on Phase-1 spawn) | Risk Tier (see § Risk Tier Framework) | | Provider key | Provider CLI | Audit pin (TBD on Phase-1 spawn) | Risk Tier (see § Risk Tier Framework) |
|---|---|---|---| |---|---|---|---|
| `anthropic` | `claude -p` from `@anthropic-ai/claude-code` | inherits OCP's `cli.js` 2.1.89 audit pin at fork; OLP-side pin: `@anthropic-ai/claude-code` v2.1.89 (observed at D4 — `lib/providers/anthropic.mjs` header). Re-evaluate post-2026-06-15 per One-shot Triggered Audit above. | D (pre-2026-06-15) / re-evaluate post-2026-06-15 | | `anthropic` | `claude -p` from `@anthropic-ai/claude-code` | inherits OCP's `cli.js` 2.1.89 audit pin at fork; OLP-side pin: `@anthropic-ai/claude-code` v2.1.89 (observed at D4 — `lib/providers/anthropic.mjs` header); transcript artifact: `docs/provider-audits/anthropic.md` (captured 2026-05-24). Re-evaluate post-2026-06-15 per One-shot Triggered Audit above. | D (pre-2026-06-15) / re-evaluate post-2026-06-15 |
| `openai` | `codex exec --json` from OpenAI Codex CLI | Codex CLI reference page: https://developers.openai.com/codex/cli/reference (retrieved 2026-05-23 — §§ "codex exec [flags] PROMPT", "--json / --experimental-json", "--model, -m"; D6 WebFetch-verified reachable). Secondary authority: https://developers.openai.com/codex/cli/features §§ "Supported Models", "Automation". | D | | `openai` | `codex exec --json` from OpenAI Codex CLI | Codex CLI reference page: https://developers.openai.com/codex/cli/reference (retrieved 2026-05-23 — §§ "codex exec [flags] PROMPT", "--json / --experimental-json", "--model, -m"; D6 WebFetch-verified reachable). Secondary authority: https://developers.openai.com/codex/cli/features §§ "Supported Models", "Automation". | D |
| `mistral` | `vibe --prompt --output streaming` from Mistral Vibe CLI | Mistral Vibe terminal quickstart: https://docs.mistral.ai/mistral-vibe/terminal/quickstart (retrieved 2026-05-23 — § "--prompt flag triggers programmatic mode; --output selects format (text, json, streaming)"; D8 WebFetch-verified reachable). `--output streaming` selected (not `--output json`) because DOCS-1 § "Output Format Options" explicitly states `json` emits a single blob at the end — incompatible with the line-buffered NDJSON parser in `lib/providers/mistral.mjs`. `streaming` emits newline-delimited JSON per message, which the parser requires. See plugin header (lines 360-369). Configuration authority: https://docs.mistral.ai/mistral-vibe/terminal/configuration (§§ auth file `~/.vibe/.env`, `MISTRAL_API_KEY` env var). | D | | `mistral` | `vibe --prompt --output streaming` from Mistral Vibe CLI | Mistral Vibe terminal quickstart: https://docs.mistral.ai/mistral-vibe/terminal/quickstart (retrieved 2026-05-23 — § "--prompt flag triggers programmatic mode; --output selects format (text, json, streaming)"; D8 WebFetch-verified reachable). `--output streaming` selected (not `--output json`) because DOCS-1 § "Output Format Options" explicitly states `json` emits a single blob at the end — incompatible with the line-buffered NDJSON parser in `lib/providers/mistral.mjs`. `streaming` emits newline-delimited JSON per message, which the parser requires. See plugin header (lines 360-369). Configuration authority: https://docs.mistral.ai/mistral-vibe/terminal/configuration (§§ auth file `~/.vibe/.env`, `MISTRAL_API_KEY` env var). | D |
| `grok` | `grok -p --output-format streaming-json` (xAI Build) | TBD at Phase 8+ enable | C | | `grok` | `grok -p --output-format streaming-json` (xAI Build) | TBD at Phase 8+ enable | C |
@@ -198,6 +198,16 @@ In addition to the recurring 14 May audit below, the following one-shot audits a
Any future Rule 3 deviation lands here as a numbered exception with PR link, reviewer, and rationale. Any future Rule 3 deviation lands here as a numbered exception with PR link, reviewer, and rationale.
### Controlled deviations (entry-surface scope)
This subsection enumerates entry-surface behaviours that intentionally extend beyond the OpenAI `/v1/chat/completions` and `/v1/models` specifications. Each entry is a **controlled deviation**: a documented, reviewed extension that ships under Rule 2(b)'s spirit (no invention without an authority) by treating `docs/openai-spec-pin.md` as the formal contract for the deviation. The contract there is binding; this list is the index.
1. **`/v1/models` alias entries** — *Issue #13 (D36)*. The OpenAI `/v1/models` specification (https://platform.openai.com/docs/api-reference/models/list) enumerates one entry per canonical model ID. OLP's `/v1/models` response additionally surfaces alias entries (e.g. `claude`, `sonnet`, `opus`, `haiku` alongside the canonical `claude-opus-4-7` / `claude-sonnet-4-6` / `claude-haiku-4-5`). Alias entries use `id: <alias-string>`, `object: 'model'`, `owned_by: <same provider key as canonical>`, and `created: <same timestamp as canonical target>` per D27 F15.
- **Rationale:** D27 F15 — onboarding friction when clients configured with `model: 'sonnet'` (a common alias used by Anthropic's own CLI and many OpenClaw-class tools) received an empty `/v1/models` response that did not surface the alias as a callable model id. Surfacing the alias makes the discovery loop usable for OpenAI-compatible clients with alias-aware UX.
- **Formal contract:** `docs/openai-spec-pin.md § GET /v1/models` is the authoritative shape for this deviation. The deviation is bounded by: (a) `owned_by` matches the canonical target's `owned_by`; (b) `created` matches the canonical target's `created`; (c) no fields are invented beyond the four OpenAI-spec entry fields (`id`, `object`, `created`, `owned_by`); (d) alias enumeration is sourced from `models-registry.json` via `getAliasMap()` — the SPOT — not hard-coded in `server.mjs`.
- **Compliance posture:** The deviation extends the response listing but does not invent fields or change field semantics. The risk vector is a hypothetical OpenAI-compatible client that asserts "one entry per canonical model" and trips on the extras; this risk is mitigated by the fact that aliases use the same `object: 'model'` shape, and any client iterating `data[]` simply sees more entries — none of which are malformed. No invention beyond what OpenAI's own `id` field already accepts as a free-form string.
- **Re-evaluation trigger:** Annual audit (14 May) re-checks whether OpenAI has shipped a formal alias-listing extension to `/v1/models` (in which case OLP migrates to it), or whether the deviation should be retired (in which case clients with alias-aware UX must migrate to the canonical IDs via the alias table).
--- ---
## Amendment Procedure ## Amendment Procedure
+12 -1
View File
@@ -7,6 +7,15 @@
## Amendments ## Amendments
### Amendment 5 — 2026-05-24: Correct § Decision filesystem layout — `vibe.mjs` → `mistral.mjs` (D36 #5)
- **Finding:** Issue #5 (D36) — § Decision filesystem layout (around line 47 of the original ADR) listed the Mistral provider plugin as `vibe.mjs` (named after the CLI binary `vibe`). The shipped file at `lib/providers/mistral.mjs` (D8) is named after the provider key, matching the established convention from the other two plugins: `anthropic.mjs` (provider key `anthropic`, CLI `claude`) and `codex.mjs` (provider key `openai`, CLI `codex`). The ADR's `vibe.mjs` entry was a drafting-time placeholder that did not get corrected when D8 landed `lib/providers/mistral.mjs`.
- **Change:** Replace `vibe.mjs # spawn `vibe --prompt --output json`` with `mistral.mjs # spawn `vibe --prompt --output streaming`` in the filesystem layout. The `--output streaming` correction also aligns the example with the actual D8 implementation (`mistral.mjs` line 377 uses `--output streaming`, not `--output json` — see D8 review-2 finding inside the plugin header).
- **Naming convention reaffirmed:** Provider plugin files are named after the **provider key** (`anthropic`, `openai`, `mistral`), not the CLI binary (`claude`, `codex`, `vibe`). Future provider plugins must follow this convention. The provider key is the load-bearing identifier — it appears in `models-registry.json`, cache keys, fallback chain configs, and ADR 0006 inclusion tables. The CLI binary name is an implementation detail that may change (e.g., a vendor rename) without affecting the rest of the system.
- **Authority:** Issue #5 (D36); naming convention established by `lib/providers/anthropic.mjs` (D4) and `lib/providers/codex.mjs` (D6) which both shipped before `lib/providers/mistral.mjs` (D8).
- **No code change:** D36 #5 is a docs-only correction. The plugin file already lives at the correct path.
- **Procedural mechanism:** CC 开发铁律 v1.6 § 10.x (D36 batch — ADR drift caught by issue-triage review of bootstrap ADRs).
### Amendment 4 — 2026-05-24: Ratify `contractVersion` as a required Provider contract field (D32 F5) ### Amendment 4 — 2026-05-24: Ratify `contractVersion` as a required Provider contract field (D32 F5)
- **Finding:** Round-4 cold-audit F5 (P3 governance omission) — `lib/providers/base.mjs` `validateProvider` enforces `p.contractVersion === '1.0'` and all three shipped plugins declare it, but the Provider contract field list in § Decision (lines ~63-74) does not include `contractVersion`. It was mentioned only in § Mitigations as a forward-looking note ("The contract is versioned. v1.0 is the subset in this ADR; future additions … require ADR amendment plus a contract-version bump. Old provider plugins continue to declare `contractVersion: '1.0'`…"), not as a required field. This is the same class of documentationimplementation gap as Amendment 1 (`maxSpawnTimeMs` retroactive sync). - **Finding:** Round-4 cold-audit F5 (P3 governance omission) — `lib/providers/base.mjs` `validateProvider` enforces `p.contractVersion === '1.0'` and all three shipped plugins declare it, but the Provider contract field list in § Decision (lines ~63-74) does not include `contractVersion`. It was mentioned only in § Mitigations as a forward-looking note ("The contract is versioned. v1.0 is the subset in this ADR; future additions … require ADR amendment plus a contract-version bump. Old provider plugins continue to declare `contractVersion: '1.0'`…"), not as a required field. This is the same class of documentationimplementation gap as Amendment 1 (`maxSpawnTimeMs` retroactive sync).
@@ -60,7 +69,9 @@ lib/providers/
index.mjs # static registry (enumeration of in-tree providers) index.mjs # static registry (enumeration of in-tree providers)
anthropic.mjs # spawn `claude -p` — port of OCP server.mjs spawn logic anthropic.mjs # spawn `claude -p` — port of OCP server.mjs spawn logic
codex.mjs # spawn `codex exec --json` codex.mjs # spawn `codex exec --json`
vibe.mjs # spawn `vibe --prompt --output json` mistral.mjs # spawn `vibe --prompt --output streaming` (file named after
# provider key per the convention established by
# anthropic.mjs / codex.mjs — see Amendment 5)
grok.mjs # spawn `grok -p --output-format streaming-json` (optional) grok.mjs # spawn `grok -p --output-format streaming-json` (optional)
kimi.mjs # spawn `kimi -p --output-format stream-json` (optional) kimi.mjs # spawn `kimi -p --output-format stream-json` (optional)
minimax.mjs # tier-2 optional, default-disabled minimax.mjs # tier-2 optional, default-disabled
+32
View File
@@ -136,6 +136,38 @@ Each entry: `{ "id": "<model-id>", "object": "model", "created": <ts>, "owned_by
no invented fields (per D27 F15). Alias entries are also surfaced as separate list members no invented fields (per D27 F15). Alias entries are also surfaced as separate list members
(per D27 F15 alias surfacing). (per D27 F15 alias surfacing).
**Alias surfacing — controlled deviation (D36 #13).** OpenAI's `/v1/models` spec
enumerates one entry per canonical model ID; OLP additionally surfaces alias entries
(e.g. `claude`, `sonnet`, `opus`, `haiku` alongside their canonical Anthropic targets).
This is a documented deviation from strict spec parity. It is governed by
`ALIGNMENT.md § Class-specific Exceptions → Controlled deviations (entry-surface scope)`,
which references this section as the formal contract.
The alias-entry contract:
| Field | Value for alias entry |
|---|---|
| `id` | the alias string (e.g. `'sonnet'`) — same shape as canonical entries |
| `object` | `'model'` — same as canonical entries |
| `created` | identical to the canonical target's `created` timestamp (per F12) |
| `owned_by` | identical to the canonical target's `owned_by` (i.e. the provider key) |
The alias list is sourced from `models-registry.json` via `getAliasMap()` in
`lib/providers/index.mjs` — the SPOT for alias-aware routing. `server.mjs handleModels`
appends alias entries to the canonical list only when the alias's canonical target's
provider is currently in `loadedProviders`. No fields beyond the four OpenAI-spec fields
are added on alias entries.
**Rationale (D27 F15):** Onboarding gap. Clients configured with `model: 'sonnet'` (a
common alias used by Anthropic's own CLI and OpenClaw-class tools) previously received
an empty `/v1/models` response that did not surface the alias as a callable model id.
Surfacing the alias makes the discovery loop usable for OpenAI-compatible clients with
alias-aware UX.
**Forward path:** Annual audit (14 May) re-checks whether OpenAI has shipped a formal
alias-listing extension to `/v1/models`. If so, OLP migrates the alias surface to that
shape. If not, the deviation continues unchanged.
**`created` field stability (F12 round-5 cold-audit):** OpenAI spec treats `created` as a **`created` field stability (F12 round-5 cold-audit):** OpenAI spec treats `created` as a
stable per-model attribute, not a request-time value. `server.mjs handleModels` uses stable per-model attribute, not a request-time value. `server.mjs handleModels` uses
`getModelCreated(modelId)` (from `lib/providers/index.mjs`) which reads the per-entry `getModelCreated(modelId)` (from `lib/providers/index.mjs`) which reads the per-entry
+124
View File
@@ -0,0 +1,124 @@
# Anthropic provider — version-capture artifact
- **Provider key:** `anthropic`
- **Plugin file:** `lib/providers/anthropic.mjs`
- **Last capture:** 2026-05-24 (D36 #15)
- **Capture host:** project maintainer's primary workstation (home-mac)
- **Status:** living artifact — re-capture at every plugin touch, or annually
during the 14 May Annual Alignment Audit, whichever comes first.
This artifact closes the circular-citation finding from Round-6 (issue #15):
ALIGNMENT.md § Provider Authority Pins anthropic row cites `@anthropic-ai/claude-code`
v2.1.89 (observed at D4) without an independent transcript; the plugin header cited
the observation date but pointed back to ALIGNMENT.md. This file is the in-repo
transcript artifact that grounds the OLP-side claim.
---
## Observed `claude --version`
The live binary on the maintainer's workstation today is:
```
2.1.132 (Claude Code)
```
Captured by running `claude --version` in a non-interactive shell on 2026-05-24.
## Plugin-pinned version
```
@anthropic-ai/claude-code v2.1.89
```
Source: ALIGNMENT.md § Authorities § "Provider authority pins" row `anthropic`
("OLP-side pin: `@anthropic-ai/claude-code` v2.1.89 (observed at D4 —
`lib/providers/anthropic.mjs` header)"). This is the version observed inside
the OLP-side D4 work; the plugin was authored against this version's flag
surface.
## Version drift note
The pinned version (v2.1.89, D4) and the live binary (v2.1.132, today) differ
because the `claude` CLI has continued to ship updates since D4. This drift is
within tolerance for the v0.1 baseline:
- The CLI flags OLP consumes — `-p`, `--output-format=text`,
`--no-session-persistence`, `--model`, `--debug` — are all still present and
semantically unchanged in v2.1.132 (verified today via `claude -p --help`
— see "Flag surface captured today" below).
- The pin in ALIGNMENT.md is conservative by design — it names the version OLP
was authored against, not the highest version known to work. Re-pinning to
v2.1.132 (or whichever version is current) is the right action at the next
Anthropic-plugin touch, when a reviewer can confirm no regressions.
- Re-audit recommended at: (a) next material change to
`lib/providers/anthropic.mjs`, OR (b) 14 May 2027 Annual Alignment Audit,
OR (c) the post-2026-06-15 one-shot triggered audit (ALIGNMENT.md
§ One-shot Triggered Audits) — whichever comes first.
## Sample invocation
The Anthropic plugin spawns the CLI with this argument shape (see
`lib/providers/anthropic.mjs` § `buildClaudeArgs` and `_spawnAndStream`):
```
claude -p --output-format text --no-session-persistence --model <model> [--debug]
```
- `-p` puts the CLI in non-interactive (print-and-exit) mode.
- `--output-format text` selects plain-text stdout. The plugin parses stdout as
plain text (no NDJSON envelope).
- `--no-session-persistence` disables session storage so OLP remains stateless
(per ADR 0001 § Non-mission — OLP is not a conversation-state store).
- `--model <model>` is forwarded from the IR's `model` field.
- `--debug` is added only when `OLP_DEBUG_CLAUDE` env is set, for development.
The prompt is written to the CLI's stdin (`messagesToPrompt(ir.messages)` from
`anthropic.mjs`), not passed as a positional argument.
## Flag surface captured today
Excerpt from `claude -p --help` on host home-mac on 2026-05-24 (v2.1.132). Only
the flags relevant to OLP's invocation are reproduced; the full help is much
larger.
| Flag | Description (verbatim, abridged) |
|---|---|
| `-p, --print` | Print response and exit (useful for pipes). |
| `--output-format <format>` | Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming) — choices: "text", "json", "stream-json". |
| `--no-session-persistence` | Disable session persistence — sessions will not be saved to disk and cannot be resumed (only works with --print). |
| `--model <model>` | Model for the current session. Provide an alias for the latest model (e.g. 'sonnet' or 'opus') or a model's full name (e.g. 'claude-sonnet-4-6'). |
| `-d, --debug [filter]` | Enable debug mode with optional category filtering (e.g., "api,hooks" or "!1p,!file"). |
All four load-bearing flags (`-p`, `--output-format`, `--no-session-persistence`,
`--model`) are present and accept the same value formats the OLP plugin uses.
The `--debug` flag is also present with the same semantics.
## Citation cross-references
- ALIGNMENT.md § Authorities § "Provider authority pins" anthropic row — names
this file as the transcript-artifact pin.
- `lib/providers/anthropic.mjs` header lines 1-50 — names this file as the
version-capture artifact (D26 F18 follow-up).
- ADR 0001 § Mission inheritance — establishes `claude -p` as Authority 1
source for the `anthropic` plugin.
- ADR 0005 Amendment 5 (D31 F11) — clarifies the wire limitation of
`claude -p --output-format text` w.r.t. cache_control marker delegation.
## Recapture procedure
When this artifact is next refreshed (per the "Version drift note" trigger
list), the maintainer should:
1. Run `claude --version` on the project maintainer's primary workstation.
2. Run `claude -p --help` and verify that `-p`, `--output-format`,
`--no-session-persistence`, `--model`, and `--debug` are all still present
with the same value formats.
3. Update the "Last capture", "Observed `claude --version`", and "Flag surface
captured today" sections with the new values.
4. If any flag's semantic has changed (not just a version bump), file an ADR
amendment on `lib/providers/anthropic.mjs` Authority 1 source and pin the
new version in ALIGNMENT.md.
5. If `claude -p --help` no longer enumerates one of OLP's load-bearing flags,
the plugin is broken against the new CLI — file a deletion or migration PR
per ALIGNMENT.md Rule 4 (Unalignable Plugins / Fields Are Deleted).
+6
View File
@@ -9,6 +9,12 @@
* present at D4 implementation (captured at D4 implementation per ALIGNMENT.md * present at D4 implementation (captured at D4 implementation per ALIGNMENT.md
* Rule 5 — the circular ALIGNMENT.md ↔ plugin header citation is resolved by * Rule 5 — the circular ALIGNMENT.md ↔ plugin header citation is resolved by
* this in-plugin record of the OLP-side observation). * this in-plugin record of the OLP-side observation).
* D36 #15: See docs/provider-audits/anthropic.md for the version-capture
* artifact (single living document — captured 2026-05-24; re-capture at every
* plugin touch or annual audit). The artifact records the live `claude --version`
* today (v2.1.132) versus the plugin pin (v2.1.89, D4) and verifies that the
* load-bearing flags (-p, --output-format, --no-session-persistence, --model,
* --debug) are all still present and semantically unchanged in the current binary.
* *
* Spawn pattern ported from: * Spawn pattern ported from:
* OCP server.mjs:384-414 (buildCliArgs — -p / --model / --output-format / --no-session-persistence) * OCP server.mjs:384-414 (buildCliArgs — -p / --model / --output-format / --no-session-persistence)
+15 -10
View File
@@ -142,17 +142,22 @@
* D-later E2E will capture real `vibe --output json` stdout and pin the * D-later E2E will capture real `vibe --output json` stdout and pin the
* actual field names; mismatched fields will be corrected then. * actual field names; mismatched fields will be corrected then.
* *
* A5 (model flag — UNPINNED-D-later-verifies): * A5 (model flag — CONFIRMED-NOT-APPLICABLE):
* Name: model_flag * Name: model_flag
* Status: UNPINNED-D-later-verifies * Status: CONFIRMED-NOT-APPLICABLE
* Basis: DOCS-3 mentions model selection via "/config" inside the interactive * Basis: DeepWiki (DOCS-4) full CLI command flag enumeration confirms that
* Vibe UI. The quickstart (DOCS-1) does not show a `--model` CLI flag for * `vibe` has no `--model` flag in programmatic mode. Model selection happens
* programmatic mode. OLP does NOT pass `--model` in the spawn args at D8 * exclusively via `~/.vibe/config.toml` (set interactively via the `/config`
* because no CLI reference confirms this flag exists on the `vibe` command * command inside Vibe per DOCS-3) — there is no CLI-flag surface OLP can use
* (per ALIGNMENT.md Rule 2: "if the underlying authority does not perform * to pass `model` per-request. ALIGNMENT.md Rule 2: the underlying authority
* the operation, the PR must state this explicitly"). * does not perform the operation, so OLP must not invent one. The IR's
* D-later E2E: run `vibe --help` to enumerate all flags; if --model exists * `model` field is used by OLP for routing only; the Vibe CLI will use
* and the flag name is confirmed, add it to spawn args with the model ID. * whatever model is configured at the user level in `~/.vibe/config.toml`.
* Pinning source: DeepWiki CLI commands reference enumeration (DOCS-4).
* See also `irToMistral` (line 371-374) which records the same finding at
* the spawn-args construction site.
* (D36 #6: status flipped from UNPINNED-D-later-verifies → CONFIRMED-NOT-APPLICABLE.
* Header status now matches the spawn-site finding that was already in place at D8.)
* *
* A6 (exact model IDs — UNPINNED-D-later-verifies): * A6 (exact model IDs — UNPINNED-D-later-verifies):
* Name: model_ids * Name: model_ids
+20
View File
@@ -425,6 +425,26 @@ async function handleChatCompletions(req, res) {
const hasCacheControlMarkers = const hasCacheControlMarkers =
hasCacheControl(ir) || extractCacheControlMarkers(body?.messages ?? []).length > 0; hasCacheControl(ir) || extractCacheControlMarkers(body?.messages ?? []).length > 0;
// D36 #2 (ADR 0005 § D2): when cache_control markers are present AND at least one
// hop in the chain is non-Anthropic, the markers are noop'd for those hops.
// Per ADR 0005 § Context: "for non-Anthropic targets, the bypass markers are
// noop'd (logged once per request at debug level so users can see they were
// ignored)." Fires at most once per request, gated on (markers AND mixed/non-anthropic
// chain). No log when no markers, or when every chain hop is Anthropic.
if (hasCacheControlMarkers && chain.some(hop => hop.provider !== 'anthropic')) {
// marker_count sums body-side and IR-side markers. At v0.1 the IR term is
// structurally 0 (openAIToIR strips cache_control). When a future ADR 0003
// amendment activates cache_control in the IR whitelist, both terms will be
// non-zero for the same logical marker set → revisit to avoid 2× counting.
const markerCount =
extractCacheControlMarkers(body?.messages ?? []).length +
extractCacheControlMarkers(ir.messages ?? []).length;
logEvent('debug', 'cache_control_partial_noop', {
chain: chain.map(hop => hop.provider),
marker_count: markerCount,
});
}
/** /**
* Returns true if OLP's response cache should be bypassed for the given hop. * Returns true if OLP's response cache should be bypassed for the given hop.
* Per ADR 0005 § D2: bypass only when provider is Anthropic AND markers present. * Per ADR 0005 § D2: bypass only when provider is Anthropic AND markers present.
+370
View File
@@ -1543,6 +1543,160 @@ describe('Cache layer — computeCacheKey (Suite 9)', () => {
}); });
}); });
// ── D36 #14: cache_control slot determinism regression ─────────────────────
//
// Context (issue #14 + ADR 0005 Amendment 4):
// computeCacheKey at lib/cache/keys.mjs:~224 calls extractCacheControlMarkers
// on ir.messages. openAIToIR (lib/ir/openai-to-ir.mjs translateMessage) does
// NOT whitelist cache_control, so v0.1 IRs constructed from OpenAI requests
// always have markers stripped → the slot is structurally null. D27 F10
// Amendment 4 acknowledged this is forward-compatible: when a future ADR 0003
// amendment adds cache_control to the IR field set, the slot starts carrying
// meaningful data without a cache-key schema change.
//
// What this suite guards:
// 1. The cache_control slot IS populated when markers are present on the IR
// (forward-compat scenario — markers attached directly to an IR object
// bypassing the openAIToIR translation).
// 2. Computing the key twice with the same marker payload yields identical
// keys (determinism — bedrock invariant per ADR 0005 § cache key stability).
// 3. The slot is included in the composition — two IRs differing ONLY in
// cache_control markers produce different keys.
//
// Future activation contract (when ADR 0003 adds cache_control to the IR
// whitelist and openAIToIR starts preserving markers):
// - extractCacheControlMarkers must continue to return markers in messages-
// iteration order; nested-in-content markers follow the outer message in
// positional order (see lib/cache/keys.mjs:126-148).
// - The current implementation JSON.stringifies the marker array verbatim
// (no pre-sort) — determinism relies on messages-array order being stable
// across translations and on per-marker object key insertion order being
// stable (which it is for objects authored at one source site).
// - If a future amendment introduces semantic equivalences across marker
// orderings (e.g., "two markers in different positions should still hit
// the same cache entry"), the implementation must add a sortMarkers helper
// before serialization. This test would then need an update to assert the
// normalized-order behavior. As of D36, no such equivalence is claimed; the
// test asserts the strict "same input → same output" determinism only.
//
// Risk path chosen: option (a) test-only (no code change in lib/cache/keys.mjs).
// Rationale: the dead-code path is unreachable from openAIToIR at v0.1, so a
// helper added now would have zero callers; preferred to defer the helper until
// the IR amendment actually activates the slot. Per ALIGNMENT.md Rule 2 (No
// Invention), shipping a sortMarkers helper today would be invention without a
// caller authority. Adding tests is risk-free; adding helpers is not.
describe('D36 #14 — cache_control slot determinism regression', () => {
// Construct an IR with synthetic cache_control markers attached directly
// (bypass openAIToIR which strips them at v0.1).
function makeIRWithMarkers(markers, opts = {}) {
const messages = opts.messages ?? [
{ role: 'user', content: 'sample-prompt' },
];
// Attach the first marker to the first message; if a second marker is
// provided, attach it to the second message (or to message 0 nested in a
// content array, depending on opts.nestSecond).
const annotated = messages.map((m, idx) => {
if (idx === 0 && markers[0]) {
return { ...m, cache_control: markers[0] };
}
if (idx === 1 && markers[1]) {
return { ...m, cache_control: markers[1] };
}
return m;
});
return makeIR({ messages: annotated, ...opts.irOverrides });
}
it('D36 #14a: cache_control slot is populated when markers are present on the IR', () => {
// The slot is normally null at v0.1 (openAIToIR strips). When markers ARE
// present (e.g. an IR constructed directly with markers, or after a future
// ADR 0003 amendment preserves them), the slot carries the markers.
const irNoMarkers = makeIR({ messages: [{ role: 'user', content: 'hi' }] });
const irWithMarker = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [{ role: 'user', content: 'hi' }],
});
const kNo = computeCacheKey('anthropic', 'claude-haiku-4-5', irNoMarkers);
const kYes = computeCacheKey('anthropic', 'claude-haiku-4-5', irWithMarker);
// Different keys: marker presence is observable in the cache key.
assert.notEqual(kNo, kYes,
'IR with cache_control markers must produce a different cache key from IR without markers');
assert.equal(typeof kYes, 'string');
assert.equal(kYes.length, 64);
});
it('D36 #14b: cache key is deterministic across two computations on the same IR with markers', () => {
// The bedrock invariant: same inputs → same key. The cache_control slot
// must not introduce nondeterminism (e.g., timestamp, random, iteration
// order from a Map).
const ir = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [
{ role: 'user', content: 'first turn' },
{ role: 'assistant', content: 'first reply' },
],
});
const k1 = computeCacheKey('anthropic', 'claude-haiku-4-5', ir);
const k2 = computeCacheKey('anthropic', 'claude-haiku-4-5', ir);
assert.equal(k1, k2, 'cache key must be deterministic for the same IR with markers');
});
it('D36 #14c: same markers in the same positional order produce the same key', () => {
// Two IRs constructed independently with the same marker payload at the
// same positions must produce the same key. This is the forward-activation
// contract: when openAIToIR preserves markers, two identical OpenAI
// requests must produce identical cache keys.
const irA = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [{ role: 'user', content: 'hello' }],
});
const irB = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [{ role: 'user', content: 'hello' }],
});
const kA = computeCacheKey('anthropic', 'claude-haiku-4-5', irA);
const kB = computeCacheKey('anthropic', 'claude-haiku-4-5', irB);
assert.equal(kA, kB,
'two independently-constructed IRs with identical marker payloads must share a cache key');
});
it('D36 #14d: markers nested in content array participate in the cache key (extractCacheControlMarkers contract)', () => {
// extractCacheControlMarkers (lib/cache/keys.mjs:126-148) finds markers at
// both message top-level AND nested inside content array parts. The cache
// key must reflect both surfaces. This is the slot's full forward-compat
// contract — when the IR carries cache_control, OLP must observe it on the
// wire shape the IR provides, including the OpenAI-style content-array
// nesting that Anthropic prompt-caching uses in production.
const irTopLevel = makeIR({
messages: [
{ role: 'user', content: 'hi', cache_control: { type: 'ephemeral' } },
],
});
const irNested = makeIR({
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'hi', cache_control: { type: 'ephemeral' } },
],
},
],
});
const irNoMarkers = makeIR({
messages: [{ role: 'user', content: 'hi' }],
});
const kTop = computeCacheKey('anthropic', 'claude-haiku-4-5', irTopLevel);
const kNested = computeCacheKey('anthropic', 'claude-haiku-4-5', irNested);
const kNone = computeCacheKey('anthropic', 'claude-haiku-4-5', irNoMarkers);
// Both marker surfaces must register against the key (be observably distinct
// from the no-markers case).
assert.notEqual(kTop, kNone, 'top-level cache_control marker must affect cache key');
assert.notEqual(kNested, kNone, 'nested cache_control marker must affect cache key');
});
});
describe('Cache layer — extractCacheControlMarkers + hasCacheControl (Suite 9 cont.)', () => { describe('Cache layer — extractCacheControlMarkers + hasCacheControl (Suite 9 cont.)', () => {
// ── Test 9: extractCacheControlMarkers — top-level ─────────────────── // ── Test 9: extractCacheControlMarkers — top-level ───────────────────
@@ -2425,6 +2579,222 @@ describe('D13 — cache_control per-hop bypass correctness (Suite 9d)', () => {
}); });
}); });
// ── Suite 9f: D36 #2 cache_control partial-noop debug log ────────────────
//
// Authority: ADR 0005 § Context — "for non-Anthropic targets, the bypass
// markers are noop'd (logged once per request at debug level so users can
// see they were ignored)."
//
// Contract: at request entry in handleChatCompletions, after hasCacheControlMarkers
// is computed, emit ONE logEvent('debug', 'cache_control_partial_noop', { chain, marker_count })
// IF AND ONLY IF (a) markers are present AND (b) at least one chain hop is
// non-Anthropic. The log is suppressed when no markers, or when every hop is
// Anthropic.
//
// Implementation strategy: monkeypatch process.stdout.write (debug events go
// to stdout per the logEvent helper in server.mjs:56-63) and grep for the
// event name across the captured writes.
describe('D36 #2 — cache_control partial-noop debug log (Suite 9f)', () => {
let serverD36;
let portD36;
let savedAnthropicTokenD36;
let savedCodexAuthPathD36;
let suiteCodexAuthFileD36;
// ── stdout capture helpers ───────────────────────────────────────────
let stdoutWrites = [];
let origStdoutWrite = null;
function startStdoutCapture() {
stdoutWrites = [];
origStdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = (chunk, ...rest) => {
const s = typeof chunk === 'string'
? chunk
: (Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk));
stdoutWrites.push(s);
return origStdoutWrite(chunk, ...rest);
};
}
function stopStdoutCapture() {
if (origStdoutWrite) {
process.stdout.write = origStdoutWrite;
origStdoutWrite = null;
}
}
/** Returns array of parsed JSON events whose `event` field === eventName. */
function findEvents(eventName) {
const found = [];
for (const w of stdoutWrites) {
for (const line of w.split('\n')) {
if (!line.trim()) continue;
if (!line.includes(`"event":"${eventName}"`)) continue;
try {
const parsed = JSON.parse(line);
if (parsed?.event === eventName) found.push(parsed);
} catch { /* not JSON — ignore */ }
}
}
return found;
}
before(async () => {
savedAnthropicTokenD36 = process.env.CLAUDE_CODE_OAUTH_TOKEN;
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'fake-anthropic-token-for-d36-suite';
const { writeFileSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const { join: pathJoin } = await import('node:path');
suiteCodexAuthFileD36 = pathJoin(tmpdir(), `olp-test-d36-codex-auth-${Date.now()}.json`);
writeFileSync(suiteCodexAuthFileD36, JSON.stringify({ accessToken: 'fake-codex-token-for-d36-suite' }), 'utf8');
savedCodexAuthPathD36 = process.env.OPENAI_CODEX_AUTH_PATH;
process.env.OPENAI_CODEX_AUTH_PATH = suiteCodexAuthFileD36;
const testProviders = loadProviders({ enabled: { anthropic: true, openai: true } });
const { loadedProviders: lp, cacheStore: cs } = await import('./server.mjs');
for (const [name, p] of testProviders) {
lp.set(name, p);
}
cs.clear();
serverD36 = createOlpServer();
portD36 = 22000 + Math.floor(Math.random() * 500);
await new Promise((resolve, reject) => {
serverD36.listen(portD36, '127.0.0.1', resolve);
serverD36.once('error', (e) => {
if (e.code === 'EADDRINUSE') {
portD36++;
serverD36.listen(portD36, '127.0.0.1', resolve);
serverD36.once('error', reject);
} else reject(e);
});
});
});
after(async () => {
stopStdoutCapture();
__resetSpawnImpl();
codexResetSpawnImpl();
if (savedAnthropicTokenD36 !== undefined) {
process.env.CLAUDE_CODE_OAUTH_TOKEN = savedAnthropicTokenD36;
} else {
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
}
if (savedCodexAuthPathD36 !== undefined) {
process.env.OPENAI_CODEX_AUTH_PATH = savedCodexAuthPathD36;
} else {
delete process.env.OPENAI_CODEX_AUTH_PATH;
}
if (suiteCodexAuthFileD36) {
const { unlinkSync } = await import('node:fs');
try { unlinkSync(suiteCodexAuthFileD36); } catch { /* ignore */ }
}
if (!serverD36) return;
return new Promise(r => serverD36.close(r));
});
it('D36 #2a: markers + non-Anthropic chain → cache_control_partial_noop log fires once', async () => {
// Route to openai (gpt-5.5) with cache_control markers present. Per ADR 0005
// § Context, the partial-noop log must fire because the chain contains a
// non-Anthropic hop and markers are present.
codexSetSpawnImpl(makeMockCodexSpawn([
JSON.stringify({ content: 'codex-out-d36-2a' }),
JSON.stringify({ type: 'stop' }),
]));
startStdoutCapture();
try {
const r = await fetch({
port: portD36,
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'gpt-5.5',
messages: [
{ role: 'user', content: 'hello-d36-2a', cache_control: { type: 'ephemeral' } },
],
},
});
assert.equal(r.status, 200, `Expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
} finally {
stopStdoutCapture();
}
const events = findEvents('cache_control_partial_noop');
assert.equal(events.length, 1, `Expected exactly 1 cache_control_partial_noop event, got ${events.length}`);
const ev = events[0];
assert.equal(ev.level, 'debug', `event level should be 'debug', got '${ev.level}'`);
assert.ok(Array.isArray(ev.chain), 'chain must be an array');
assert.ok(ev.chain.includes('openai'),
`chain should include 'openai', got: ${JSON.stringify(ev.chain)}`);
assert.equal(typeof ev.marker_count, 'number', 'marker_count must be a number');
assert.ok(ev.marker_count >= 1, `marker_count must be >= 1, got ${ev.marker_count}`);
});
it('D36 #2b: no markers + non-Anthropic chain → cache_control_partial_noop does NOT fire', async () => {
codexSetSpawnImpl(makeMockCodexSpawn([
JSON.stringify({ content: 'codex-out-d36-2b' }),
JSON.stringify({ type: 'stop' }),
]));
startStdoutCapture();
try {
const r = await fetch({
port: portD36,
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'gpt-5.5',
messages: [
{ role: 'user', content: 'hello-d36-2b' }, // no cache_control
],
},
});
assert.equal(r.status, 200, `Expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
} finally {
stopStdoutCapture();
}
const events = findEvents('cache_control_partial_noop');
assert.equal(events.length, 0,
`Expected 0 cache_control_partial_noop events when no markers; got ${events.length}: ${JSON.stringify(events)}`);
});
it('D36 #2c: markers + anthropic-only chain → cache_control_partial_noop does NOT fire', async () => {
// anthropic single-hop chain — every hop is anthropic, so the partial-noop
// log must be suppressed. The cache_bypass log (already documented) is the
// correct signal for this case, not partial_noop.
__setSpawnImpl(makeMockSpawn(['anthropic-out-d36-2c']));
startStdoutCapture();
try {
const r = await fetch({
port: portD36,
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'claude-haiku-4-5',
messages: [
{ role: 'user', content: 'hello-d36-2c', cache_control: { type: 'ephemeral' } },
],
},
});
assert.equal(r.status, 200, `Expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
} finally {
stopStdoutCapture();
}
const events = findEvents('cache_control_partial_noop');
assert.equal(events.length, 0,
`Expected 0 cache_control_partial_noop events when chain is anthropic-only; got ${events.length}: ${JSON.stringify(events)}`);
});
});
// ── Suite 11: Codex plugin (D6) ────────────────────────────────────────── // ── Suite 11: Codex plugin (D6) ──────────────────────────────────────────
// //
// All tests are UNIT tests. No real `codex` binary is invoked. // All tests are UNIT tests. No real `codex` binary is invoked.