diff --git a/AGENTS.md b/AGENTS.md index 47fab4b..8455fa0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,14 +37,16 @@ Runtime: Node.js (ESM, `.mjs` throughout). No build step. No bundler. `server.mj - `lib/ir/` — Intermediate Representation definition + serializers. Governed by ADR 0003. - `lib/cache/` — content-addressed cache layer (per-key isolation, `cache_control` bypass, chunked stream replay, singleflight). Governed by ADR 0005. - `lib/fallback/` — fallback engine (trigger detection, chain advancement, idempotent-failure safety, header annotation). Governed by ADR 0004. -- `lib/keys.mjs` — multi-key auth, per-key namespacing, audit log. Carries OCP's per-key isolation model into OLP. -- `dashboard.html` — owner-only multi-provider dashboard (quota panels, fallback rate, cache hit rate). +- `lib/keys.mjs` — multi-key auth, per-key namespacing, audit log. Carries OCP's per-key isolation model into OLP. **📋 Planned (Phase 2) — not yet authored.** +- `dashboard.html` — owner-only multi-provider dashboard (quota panels, fallback rate, cache hit rate). **📋 Planned (Phase 6) — not yet authored.** - `models-registry.json` — single source of truth for `(provider, model) → metadata`. SPOT. - `ALIGNMENT.md` — the constitution. Binding for any plugin / entry-surface / IR change. - `docs/adr/` — Architecture Decision Records. Read the index in `docs/adr/README.md` before proposing governance, SPOT, or contract changes. - `.github/workflows/alignment.yml` — CI blacklist grep + per-provider citation soft check; fails the build on known-hallucinated tokens. - `CLAUDE.md` — Claude-Code-specific session instructions + `release_kit` overlay (Iron Rule 5.5). +**Implementation status note (as of 2026-05-24):** Files marked 📋 above are designed and documented but not yet on disk. For the full status table see `README.md § "Implementation status"`. Do not attempt to read or import these files — they will not be found. The shipped set as of Phase 1 is: `server.mjs`, `lib/ir/`, `lib/providers/{anthropic,codex,mistral}.mjs`, `lib/cache/{keys,store}.mjs`, `lib/fallback/engine.mjs`, `models-registry.json`, `test-features.mjs`. + --- ## Project-specific constraints @@ -52,7 +54,7 @@ Runtime: Node.js (ESM, `.mjs` throughout). No build step. No bundler. `server.mj - **`ALIGNMENT.md` is binding.** Any PR touching a provider plugin, the entry surface, or the IR must cite the relevant authority (provider CLI documentation / OpenAI spec URL / ADR number) in the commit body and PR description. See `CLAUDE.md` § "Hard requirements for plugin / server.mjs changes" and `ALIGNMENT.md` Rules 1, 2, 5. - **Alignment CI is not suppressible.** The `alignment.yml` workflow greps for known-hallucinated tokens (currently carrying OCP's `api.anthropic.com/api/oauth/usage` as a transitive guardrail) and runs per-provider sanity checks. Adding new blacklist tokens is done via PR amendment to `alignment.yml`; removing entries requires an `ALIGNMENT.md` amendment PR. - **No self-approval.** Implementation author cannot merge their own PR (Iron Rule 10). A fresh-context reviewer must open the cited authority and confirm in the review comment. -- **`models-registry.json` is the only place to add/edit `(provider, model)` metadata.** Do not touch hardcoded model maps in `server.mjs`, `lib/providers/*.mjs`, or `setup.mjs`. The OLP ADR that codifies this SPOT discipline lands in Phase 1 (OCP's ADR 0003 — `models.json` SPOT — is the precedent; OLP needs its own SPOT ADR because the registry shape differs). +- **`models-registry.json` is the only place to add/edit `(provider, model)` metadata.** Do not touch hardcoded model maps in `server.mjs`, `lib/providers/*.mjs`, or `setup.mjs` (📋 `setup.mjs` is planned, not yet authored). The OLP ADR that codifies this SPOT discipline lands in Phase 1 (OCP's ADR 0003 — `models.json` SPOT — is the precedent; OLP needs its own SPOT ADR because the registry shape differs). - **Provider plugins follow the contract in ADR 0002.** A new provider plugin must implement every method on the Provider contract (`name`, `displayName`, `models`, `auth`, `spawn`, `estimateCost`, `quotaStatus`, `healthCheck`, `hints`). Partial implementations are unalignable per `ALIGNMENT.md` Rule 4. - **No anti-fingerprinting.** OLP is honest about spawning the real CLI. If a provider detects subscription-spawn proxying and bans it, the response is to drop the provider per ADR 0006, not to mask the spawn. - **No conversation state.** OLP is a stateless proxy. Memory / continuity is the client's responsibility (see ADR 0001 § Non-mission). diff --git a/ALIGNMENT.md b/ALIGNMENT.md index 71e4459..ebb79e1 100644 --- a/ALIGNMENT.md +++ b/ALIGNMENT.md @@ -65,7 +65,7 @@ OLP exposes a single external HTTP surface: `/v1/chat/completions` in OpenAI Cha Citation format for entry-surface PRs: OpenAI spec URL + the specific field, parameter, or behaviour. Example: `OpenAI Chat Completions, response_format parameter (https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format)`. -The spec pin lives in `docs/openai-spec-pin.md` (created at Phase 1; not required for v0.1 governance bootstrap to land). +The spec pin lives in `docs/openai-spec-pin.md` — deferred, not yet authored; will land before the first annual audit (target: v1.0). Not required for v0.1 governance bootstrap. ### Authority 3 — IR contract (internal interop scope) @@ -154,10 +154,10 @@ In addition to the recurring 14 May audit below, the following one-shot audits a - **Date:** 14 May each year (the anniversary of the Anthropic 2026-05-14 announcement, which is the structural trigger for OLP). - **Scope (per-provider plugin):** For each enabled provider, re-audit the provider CLI version against the pin in the Authorities table above. Re-verify that every spawn invocation, flag, and output-parser expectation in `lib/providers/.mjs` still matches that CLI version's actual behaviour. Update the pin row in this file. -- **Scope (entry surface):** Snapshot OpenAI's `/v1/chat/completions` specification and diff against the pin in `docs/openai-spec-pin.md`. For each field OLP implements, verify the spec still defines it the same way. Update the pin. +- **Scope (entry surface):** Snapshot OpenAI's `/v1/chat/completions` specification and diff against the pin in `docs/openai-spec-pin.md` (📋 deferred — not yet authored; must be created before the first annual audit). For each field OLP implements, verify the spec still defines it the same way. Update the pin. - **Scope (IR):** Diff the IR documented in ADR 0003 against the implementation in `lib/ir/`. Any drift triggers an amendment or a deletion PR. - **Scope (Risk Tier reclassifications):** For each provider, re-evaluate the risk tier against current ToS, FAQ language, and observed enforcement events from the past year. Reclassifications land as ADR 0006 amendments. -- **Output:** A signed audit note committed to `docs/alignment-audits/YYYY-05-14.md`, updating pins inline in this file as needed. +- **Output:** A signed audit note committed to `docs/alignment-audits/YYYY-05-14.md` (📋 `docs/alignment-audits/` directory does not exist yet; it is created when the first audit is conducted). Inline pin updates go into this file. - **Failure mode:** Any finding that cannot be reconciled triggers an immediate deletion PR per the Unalignable Policy. --- diff --git a/CLAUDE.md b/CLAUDE.md index 52b0eb3..203f97d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,4 +106,6 @@ release_kit: - new file / SPOT / schema → Architecture or contributor § with link bootstrap_quirk_policy: - any first-run migration quirk (e.g., from OCP) → README § "Troubleshooting" + scripts/migrate-from-ocp.mjs if applicable + # NOTE: scripts/migrate-from-ocp.mjs is planned (Phase 7), not yet authored. The scripts/ directory + # does not currently exist. References here are forward-looking; do not attempt to run this script. ``` diff --git a/README.md b/README.md index c8c5ad0..666bb90 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,32 @@ If a fallback chain is exhausted, `X-OLP-Fallback-Exhausted` lists the tried pro --- +## Implementation status (as of 2026-05-24) + +Phase 1 is in progress. This table reflects what is currently shipped vs. what is designed for later phases. + +| File / artifact | Status | Notes | +|---|---|---| +| `server.mjs` | ✅ Shipped | HTTP listener + dispatcher | +| `lib/ir/` | ✅ Shipped | IR definition + serializers (ADR 0003) | +| `lib/providers/anthropic.mjs` | ✅ Shipped | `claude -p` spawn-binary plugin | +| `lib/providers/codex.mjs` | ✅ Shipped | `codex exec --json` plugin | +| `lib/providers/mistral.mjs` | ✅ Shipped | `vibe --prompt` plugin | +| `lib/cache/keys.mjs` | ✅ Shipped | Content-addressed key computation | +| `lib/cache/store.mjs` | ✅ Shipped | In-memory Map (file-backed layout: 📋 Phase 2 storage adapter) | +| `lib/fallback/engine.mjs` | ✅ Shipped | Trigger evaluation + chain advancement (ADR 0004) | +| `models-registry.json` | ✅ Shipped | SPOT for `(provider, model)` metadata | +| `test-features.mjs` | ✅ Shipped | 328-test suite (CI: `test.yml`) | +| `lib/keys.mjs` | 📋 Planned (Phase 2) | Multi-key auth, per-key namespacing, audit log | +| `dashboard.html` | 📋 Planned (Phase 6) | Owner-only multi-provider dashboard | +| `docs/provider-caveats.md` | 📋 Planned (Phase 3+) | Lossy-translation reference; for now documented inline in each plugin header | +| `docs/openai-spec-pin.md` | 📋 Planned (Phase 1 gate) | OpenAI spec snapshot for annual audit; deferred from v0.1 bootstrap | +| `docs/alignment-audits/` | 📋 Planned | Output directory for annual alignment audits (first audit: 2027-05-14) | +| `scripts/migrate-from-ocp.mjs` | 📋 Planned (Phase 7) | OCP → OLP migration tool | +| `setup.mjs` | 📋 Planned | Setup wizard / initial config | + +--- + ## Architecture OLP is a Node.js (ESM, `.mjs`) HTTP proxy with no build step and minimal dependencies. The high-level shape: @@ -140,9 +166,9 @@ OLP is a Node.js (ESM, `.mjs`) HTTP proxy with no build step and minimal depende - **Entry surface** — `server.mjs` handles `/v1/chat/completions` and the administrative endpoints. Governed by OpenAI's `/v1/chat/completions` specification as the wire authority. See [`ALIGNMENT.md` § Authorities](./ALIGNMENT.md#authorities). - **Intermediate Representation (IR)** — `lib/ir/` normalizes between the entry surface and provider-native shapes. The IR is the lingua franca; any extension is an [ADR 0003](./docs/adr/0003-intermediate-representation.md) amendment. - **Provider plugins** — `lib/providers/.mjs`. Each plugin implements the contract in [ADR 0002 (Plugin Architecture for Providers)](./docs/adr/0002-plugin-architecture.md), spawns its CLI, and translates between IR and provider-native IO. -- **Cache layer** — `lib/cache/` is a content-addressed cache keyed on `(provider, model, messages, tools, temperature, response_format, cache_control)`. Per-key isolation, prompt-caching bypass, chunked stream replay, and singleflight. See [ADR 0005 (Cache Layer Cross-Provider Design)](./docs/adr/0005-cache-cross-provider.md). +- **Cache layer** — `lib/cache/` is a content-addressed cache keyed on `(provider, model, messages, tools, temperature, response_format, cache_control)`. Per-key isolation, prompt-caching bypass, chunked stream replay, and singleflight. See [ADR 0005 (Cache Layer Cross-Provider Design)](./docs/adr/0005-cache-cross-provider.md). Current backing store is an in-memory Map; file-backed storage is planned for Phase 2. - **Fallback engine** — `lib/fallback/` advances a configured chain one provider at a time on configured triggers, never retrying after the first response chunk has been emitted to the client. See [ADR 0004](./docs/adr/0004-fallback-engine.md). -- **Multi-key auth** — `lib/keys.mjs` carries OCP's per-OLP-key namespace isolation forward. Each OLP API key has independent quota, cache namespace, and audit log; each key declares which providers it can access. +- **Multi-key auth** — `lib/keys.mjs` (📋 planned, Phase 2) will carry OCP's per-OLP-key namespace isolation forward. Each OLP API key will have independent quota, cache namespace, and audit log; each key declares which providers it can access. Read the ADRs in `docs/adr/` in order before proposing structural changes. @@ -168,7 +194,7 @@ Full spec (decision rationale, open questions, risks): `~/.cc-rules/memory/proje ## Migration from OCP -_placeholder — `scripts/migrate-from-ocp.mjs` lands with Phase 7._ +_placeholder — `scripts/migrate-from-ocp.mjs` lands with Phase 7 (📋 planned, not yet authored)._ Anticipated user-facing flow (target: <5 minutes): diff --git a/docs/adr/0001-project-founding.md b/docs/adr/0001-project-founding.md index dc0a83f..e132783 100644 --- a/docs/adr/0001-project-founding.md +++ b/docs/adr/0001-project-founding.md @@ -53,7 +53,7 @@ OCP ADR 0005 receives a header amendment on merge of this ADR: *"Superseded in p **Negative** - OCP's `cli.js`-alignment moat (ADR 0002 + 0005) does not transfer. OLP proxies multiple CLIs, each with its own protocol authority; the `cli.js` byte-for-byte discipline becomes per-provider per-CLI discipline (see ADR 0002 and the new OLP ALIGNMENT.md). This is more surface area to keep honest, not less. -- Migration cost for the user: stop OCP, install OLP, point clients at the new port, migrate auth tokens. Spec §7 budgets this at <5 minutes via `scripts/migrate-from-ocp.mjs`. Real-world time will be higher on the first machine. +- Migration cost for the user: stop OCP, install OLP, point clients at the new port, migrate auth tokens. Spec §7 budgets this at <5 minutes via `scripts/migrate-from-ocp.mjs` (📋 planned, Phase 7 — not yet authored). Real-world time will be higher on the first machine. - The maintainer now operates two repos during OLP's pre-v1.0 phase: OCP in maintenance, OLP under active development. This is intentional — OCP serves real traffic today and cannot be deleted in advance — but doubles the release surface temporarily. **Mitigations** diff --git a/docs/adr/0003-intermediate-representation.md b/docs/adr/0003-intermediate-representation.md index 13eabb9..49466b0 100644 --- a/docs/adr/0003-intermediate-representation.md +++ b/docs/adr/0003-intermediate-representation.md @@ -65,7 +65,7 @@ for await (const irChunk of irResponseStream) { } ``` -**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`. The caveats document is referenced from `/v1/models` response metadata so clients can see provider-specific limits without reading source. +**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. @@ -76,7 +76,7 @@ for await (const irChunk of irResponseStream) { **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` + `/v1/models` metadata. 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. +- Lossy translations are documented per-provider and surfaced to clients via `docs/provider-caveats.md` (📋 planned) + `/v1/models` metadata. 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** diff --git a/docs/adr/0004-fallback-engine.md b/docs/adr/0004-fallback-engine.md index e7c0f98..a028202 100644 --- a/docs/adr/0004-fallback-engine.md +++ b/docs/adr/0004-fallback-engine.md @@ -105,7 +105,7 @@ Each fallback hop emits a structured log event with: timestamp, chain id, hop in **Mitigations** - The first-chunk rule applies at the byte level (any data written to the response stream blocks fallback), not at the semantic level (where it would require parsing). Implementation is a single boolean flag tracking whether anything has been written; the rule is auditable in a few lines of code. -- Provider plugins that cannot retrieve quota (`quotaStatus` returns null) are documented in their plugin header and in `docs/provider-caveats.md`. Soft triggers configured against such providers issue a startup warning so the user knows the trigger will never fire. +- Provider plugins that cannot retrieve quota (`quotaStatus` returns null) are documented in their plugin header and in `docs/provider-caveats.md` (📋 planned — not yet authored; until it exists, this information lives in the plugin header comment). Soft triggers configured against such providers issue a startup warning so the user knows the trigger will never fire. - The deferred trigger types (deterministic, cost-aware) are tracked in spec §8 with explicit Q-tags. Adding them later is an ADR amendment plus a contract addition to the trigger taxonomy, not a redesign. ## Alternatives considered diff --git a/docs/adr/0005-cache-cross-provider.md b/docs/adr/0005-cache-cross-provider.md index a9cfc08..fe0f012 100644 --- a/docs/adr/0005-cache-cross-provider.md +++ b/docs/adr/0005-cache-cross-provider.md @@ -61,12 +61,14 @@ key = sha256(JSON.stringify({ **Per-model isolation.** Cache entries are keyed on `(provider, model)` pair. `anthropic/claude-sonnet-4-6` and `openai/gpt-5-codex` produce distinct cache entries for identical IR messages. There is no cross-model cache sharing in v1.0; this is intentional and correct. -**D1 — per-key isolation (ported from OCP v3.13.0).** Each OLP API key has an independent cache namespace. Cache directory structure: +**D1 — per-key isolation (ported from OCP v3.13.0).** Each OLP API key has an independent cache namespace. Designed file-backed layout (target for Phase 2 storage adapter): ``` ~/.olp/cache///.json ``` The `` segment ensures per-key isolation; the `` is the first two hex chars of the key for filesystem-fanout sanity at high cache counts. +> **Implementation note (as of 2026-05-24):** The v0.1 implementation in `lib/cache/store.mjs` uses an in-memory `Map` as the backing store — no files are written to `~/.olp/cache/`. The file-backed layout described above is the designed shape; it transitions in via a Phase 2 storage adapter. Per-key isolation and singleflight (D4) are live; file persistence is not. + **D2 — `cache_control` bypass (ported, scope-expanded).** If the IR request contains Anthropic `cache_control` markers AND the active provider in the current chain hop is Anthropic, the OLP response cache is bypassed (Anthropic's own prompt cache is consulted at the provider). If the active provider is not Anthropic, the `cache_control` markers are stripped from the IR before provider translation (so they don't get passed to providers that wouldn't understand them) and a debug log entry is emitted. Future provider-specific bypass markers extend this rule by adding their own provider-conditional bypass logic. **D3 — chunked stream replay (ported).** When a cache hit occurs on a `stream: true` request, the cached response is replayed as SSE chunks with the same chunking granularity as the original spawn. This requires storing the chunk boundaries (not just the concatenated content) in the cache entry. Cache entry format includes a `chunks[]` array, each with `delta` and `timestamp` relative to spawn start; replay throttles to the original timing pattern (configurable: real-time replay vs. burst-replay; default burst-replay because real-time replay adds latency without value for cached requests).