mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
fix+docs: D32 — round-4 cleanup batch (F2/F3/F4/F5/F7/F8/F9)
cold-audit catch from 2026-05-24 (round 4) Round-4 cold-audit cleanup batch. 7 items grouped per IDR cleanup-batch convention (D19/D20/D25/D26/D30/D31 precedent). 2 P2 + 3 ADR amendments + 2 small docs/code cleanups. F10 (P3 semantic edge case) filed separately as GitHub issue #8. Changes (8 files, +200 / -38): **P2 fixes** 1. **F3 — README missing 6 provider auth env vars** (README.md): D30 fixed the `OLP_*` table but missed the auth-bearing env vars actually read by plugin code: - `CLAUDE_CODE_OAUTH_TOKEN` (anthropic.mjs:84) — highest-precedence override; bypasses keychain + .credentials.json file lookup - `OPENAI_CODEX_AUTH_PATH` (codex.mjs:163) — overrides full auth file path; when set, no other path tried - `CODEX_HOME` (codex.mjs:176) — overrides base dir; default auth path becomes `$CODEX_HOME/auth.json` - `MISTRAL_API_KEY` (mistral.mjs:260) — directly supplies API key; highest precedence per Mistral DOCS-2 - `MISTRAL_VIBE_AUTH_PATH` (mistral.mjs:265) — overrides full .env path; evaluated only when MISTRAL_API_KEY absent - `VIBE_HOME` (mistral.mjs:277) — overrides Vibe base dir; default auth path becomes `$VIBE_HOME/.env` Real onboarding-blocker fix: new users couldn't run OLP without knowing about these env vars; README now documents them in a "Per-provider auth env vars" subsection. 2. **F8 — Early-return error paths missing X-OLP-* headers** (server.mjs): ADR 0004 § Observability + README claim "every response carries the 5 X-OLP-* headers" but 503 (no-enabled-providers), 415 (wrong Content-Type), and early 400s (bad JSON, IR validation) emitted only Content-Type + Content-Length + X-OLP-Latency-Ms (D18 only wired the chain-exhausted path). Operators debugging these paths got less info than the ADR promised. New helper `olpErrorHeaders({ startMs, model })` emits the canonical "no provider attempted" defaults: X-OLP-Provider-Used: 'none' X-OLP-Model-Used: model ?? 'unknown' X-OLP-Fallback-Hops: '0' X-OLP-Cache: 'bypass' X-OLP-Latency-Ms: <delta> 6 sendError call sites updated: 415 / 400-bad-JSON / 400-IR-parse (model: undefined → 'unknown') + 503-no-providers / 503-provider- disappeared / 500-engine-programming (model: ir.model). The 502 streaming-error-before-first-chunk path correctly stays on `olpHeaders(...)` since a provider WAS attempted. **ADR amendments** 3. **F2 — ADR 0003 model-mapping example correction** (docs/adr/0003, Amendment 2): the § Required fields example claimed `claude-sonnet-4-6` → `claude-sonnet-4-6-20260301` mapping happens in the provider plugin. This was wrong: per D17 SPOT decision (commitcb86807), `irRequest.model` is passed verbatim to the CLI; each provider CLI accepts its own aliases natively. Both inline correction (in § Decision) AND new Amendment 2 (in § Amendments) added. 4. **F4 — OUTPUT_PARSE_ERROR dead code removal** (base.mjs, engine.mjs): `PROVIDER_ERROR_CODES.OUTPUT_PARSE_ERROR` and `HARD_TRIGGER_CODES.OUTPUT_PARSE_ERROR = true` were registered but ZERO plugins emit OUTPUT_PARSE_ERROR. ADR 0004 § Trigger taxonomy enumerates 4 hard-trigger categories (5xx, quota 4xx, exit-code, spawn-timeout) — OUTPUT_PARSE_ERROR was a 5th never authorized by ADR. Removed from both enums with removal-reason comments for auditability. Re-add via ADR 0004 amendment if a future plugin surfaces parse failures (cheaper than authoring an amendment for dead code now). 5. **F5 — ADR 0002 Amendment 4 ratifying contractVersion** (docs/adr/0002): `lib/providers/base.mjs:76-81` enforces `p.contractVersion === '1.0'` and all 3 plugins declare it, but ADR 0002 § Provider contract field list never named it. Same governance-violation class as D11's `maxSpawnTimeMs` retroactive sync (Amendment 1). Amendment 4 ratifies contractVersion as a required v1.0 contract field; § Provider contract field list updated to 10 fields (was 9). **Small cleanups** 6. **F7 — README API Endpoints table 📋 markers** (README.md): added Status column with ✅ Shipped (3 rows: /v1/chat/completions, /v1/models, /health) and 📋 Planned (3 rows: /cache/stats Phase 5, /v0/management/ quota Phase 6, /dashboard Phase 6). Matches D20's Implementation Status table convention. 7. **F9 — Codex parser inline assumption labels** (codex.mjs): added 5 inline `// A4:` comments on each defensive branch in `codexChunkToIR`. Pre-D32 these branches had only the top-of-function block comment; ALIGNMENT.md Speculative-Candidate condition 5 promises future implementers can grep assumption labels to find every speculative branch — D32 honors that promise for codex.mjs (mistral.mjs already had this pattern from D8). **Tests** (test-features.mjs): - 17g retitled + assertion expanded to full 5-header set (was partial) - 17h new: 415 wrong Content-Type → 5 X-OLP-* headers with 'unknown' model - 17i new: 503 no-enabled-providers → 5 X-OLP-* headers with ir.model - OUTPUT_PARSE_ERROR test removed (replaced with comment for audit trail) Test count: 400 → 401 (-1 OUTPUT_PARSE_ERROR + 2 new F8 + 1 retitled existing = net +1). **F10 filed as issue, NOT in D32**: https://github.com/dtzp555-max/olp/issues/8 — "Soft-skip + chain- exhausted: X-OLP-Provider-Used semantics ambiguity". Semantic edge case requiring soft trigger reactivation (deferred to v1.x per D22 Amendment 2); not a v0.1 user-affecting issue. Pre-commit fold-in (per evidence-first checkpoint #4): - **D32 reviewer caught a doc-accuracy bug**: README's CLAUDE_CODE_OAUTH_TOKEN row described the no-env fallback chain as "Searches keychain first, then `~/.claude/.credentials.json`" — but the actual code in anthropic.mjs:82-112 checks file FIRST, then keychain (darwin-only). Order was reversed. Folded in: corrected to "Searches `~/.claude/.credentials.json` first, then macOS keychain (darwin only)". Same class of doc-accuracy mistake as the D25 → D31 / D27 → D31 pattern: D-batch reviewer catches docs that contradict source. Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus, independent of drafter): APPROVE_WITH_MINOR. Independently verified: - All 6 F3 env vars: precedence chains traced through plugin source (caught the anthropic reversed-order — folded in) - F8 olpErrorHeaders helper correctly distinguishes "no provider" defaults from olpHeaders' "provider attempted" defaults - F8: all 6 sendError call sites use the right model arg (undefined pre-IR-parse → 'unknown'; ir.model post-IR-parse) - F8: 502 streaming-error stays on olpHeaders (provider was attempted) - F4: OUTPUT_PARSE_ERROR removed from both enums, zero plugin emit references remain, test cleanup is auditable - F5: ADR 0002 Amendment 4 matches Amendment 1's retroactive-sync structure; contract field list now 10 items - F2: D17 commitcb86807verified to match the SPOT decision claim - F7/F9: docs/code cleanups verified - F10 issue #8 verified OPEN with correct title - 401/401 tests pass 3 non-blocking suggestions (anthropic precedence column folded; F2 inline mistral `--model` caveat could be added; F4 wording asymmetry between base.mjs and engine.mjs comments) — minor polish, not folded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+35
-6
@@ -238,6 +238,31 @@ function olpHeaders({ providerUsed, modelUsed, startMs, cacheStatus = 'miss', fa
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the standard 5-header OLP diagnostic set for pre-chain error paths
|
||||
* (no provider was attempted yet). Used by sendError call sites that occur
|
||||
* before chain construction (415 wrong Content-Type, 400 bad JSON, 400 IR
|
||||
* validation failure, 503 no-enabled-providers).
|
||||
*
|
||||
* Per F8 (D32 round-4 cold-audit): ADR 0004 § Observability requires the full
|
||||
* 5-header set on every response; early error paths must emit canonical
|
||||
* "no provider attempted" defaults.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {number} opts.startMs — request start timestamp (Date.now())
|
||||
* @param {string|null|undefined} [opts.model] — ir.model if IR was parsed; undefined/null otherwise
|
||||
* @returns {Record<string,string>}
|
||||
*/
|
||||
function olpErrorHeaders({ startMs, model }) {
|
||||
return {
|
||||
'X-OLP-Provider-Used': 'none',
|
||||
'X-OLP-Model-Used': model ?? 'unknown',
|
||||
'X-OLP-Fallback-Hops': '0',
|
||||
'X-OLP-Cache': 'bypass',
|
||||
'X-OLP-Latency-Ms': String(Date.now() - startMs),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Route handlers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -320,7 +345,7 @@ async function handleChatCompletions(req, res) {
|
||||
const ct = req.headers['content-type'] ?? '';
|
||||
if (!ct.includes('application/json')) {
|
||||
return sendError(res, 415, 'Content-Type must be application/json', 'invalid_request_error',
|
||||
{ 'X-OLP-Latency-Ms': String(Date.now() - startMs) });
|
||||
olpErrorHeaders({ startMs }));
|
||||
}
|
||||
|
||||
let body;
|
||||
@@ -328,7 +353,7 @@ async function handleChatCompletions(req, res) {
|
||||
body = await readJSON(req);
|
||||
} catch (e) {
|
||||
return sendError(res, e.statusCode ?? 400, e.message, 'invalid_request_error',
|
||||
{ 'X-OLP-Latency-Ms': String(Date.now() - startMs) });
|
||||
olpErrorHeaders({ startMs }));
|
||||
}
|
||||
|
||||
// Translate OpenAI → IR (ADR 0003)
|
||||
@@ -338,7 +363,7 @@ async function handleChatCompletions(req, res) {
|
||||
} catch (e) {
|
||||
if (e instanceof BadRequestError) {
|
||||
return sendError(res, 400, e.message, 'invalid_request_error',
|
||||
{ 'X-OLP-Latency-Ms': String(Date.now() - startMs) });
|
||||
olpErrorHeaders({ startMs }));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -365,6 +390,7 @@ async function handleChatCompletions(req, res) {
|
||||
res, 503,
|
||||
`No enabled providers for model ${ir.model}. See README § Supported Providers.`,
|
||||
'no_enabled_provider',
|
||||
olpErrorHeaders({ startMs, model: ir.model }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,7 +584,8 @@ async function handleChatCompletions(req, res) {
|
||||
|
||||
if (!streamPlugin) {
|
||||
// Provider disappeared between chain build and here (edge case).
|
||||
return sendError(res, 503, `Provider ${streamProvider} is not enabled`, 'no_enabled_provider');
|
||||
return sendError(res, 503, `Provider ${streamProvider} is not enabled`, 'no_enabled_provider',
|
||||
olpErrorHeaders({ startMs, model: ir.model }));
|
||||
}
|
||||
|
||||
const streamHeaders = olpHeaders({
|
||||
@@ -676,7 +703,8 @@ async function handleChatCompletions(req, res) {
|
||||
error: e.message,
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
sendError(res, 502, e.message ?? 'Provider error', 'provider_error');
|
||||
sendError(res, 502, e.message ?? 'Provider error', 'provider_error',
|
||||
olpHeaders({ providerUsed: streamProvider, modelUsed: streamModel, startMs, cacheStatus: 'miss', fallbackHops: 0 }));
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
@@ -693,7 +721,8 @@ async function handleChatCompletions(req, res) {
|
||||
} catch (e) {
|
||||
// executeWithFallback throws only on programming errors (empty chain).
|
||||
logEvent('error', 'fallback_engine_error', { error: e.message });
|
||||
return sendError(res, 500, 'Internal server error', 'internal_error');
|
||||
return sendError(res, 500, 'Internal server error', 'internal_error',
|
||||
olpErrorHeaders({ startMs, model: ir.model }));
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
Reference in New Issue
Block a user