mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-22 13:35:08 +00:00
fix(structured): crash-safe validation façade — deep model reply → refusal, not a 500 (closes #181)
#153's cyclic-$ref guard caps the REF-chain depth but not the DATA depth: validateJsonSchema recurses on the value's nesting (properties/items/additionalProps), so a model reply nested ~2000+ levels overflowed the stack with a RangeError, which handleChatCompletions caught as a generic HTTP 500 instead of the spec-correct refusal. (Found in the #153 final review, filed as #181; ≤1 spawn, no crash, no client-only trigger — the value always comes from the model reply.) New exported validateJsonSchemaSafe() wraps the validator: ANY throw (the deep-data RangeError, or any future recursion hazard) becomes a single validation error, so the structured-output retry loop treats a pathological reply as "did not validate" → refusal. A well-formed reply is byte-identical (passes the inner errors through). runStructuredCompletion calls the safe façade. Chose the wrapper over threading a data-depth counter through six recursive call sites: it protects every internal path at once (impossible to miss one) and stays deterministically testable — a 6000-deep fixture reliably overflows on any platform. Tests: +2, mutation-proven (revert the wrapper to a bare call → the deep test throws RED). Suite 431/0. Rule 2: OCP-internal validation, no wire change, no cli.js citation. Closes #181 Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude <claude-opus-4-8> <noreply@anthropic.com>
parent
788cbbcd99
commit
4143993e3b
+5
-2
@@ -42,7 +42,7 @@ import { dirname, join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { validateKey, recordUsage, getUsageByKey, getUsageTimeline, getRecentUsage, createKey, listKeys, revokeKey, closeDb, checkQuota, updateKeyQuota, getKeyQuota, findKey, cacheHash, getCachedResponse, setCachedResponse, clearCache, getCacheStats, hasCacheControl, singleflight, getInflightStats } from "./keys.mjs";
|
||||
import { DEFAULT_PORT } from "./lib/constants.mjs";
|
||||
import { StructuredOutputError, detectStructuredOutput, validateJsonSchema, extractJsonPayload, structuredSystemInstruction, resolveMaxAttempts } from "./lib/structured-output.mjs";
|
||||
import { StructuredOutputError, detectStructuredOutput, validateJsonSchema, validateJsonSchemaSafe, extractJsonPayload, structuredSystemInstruction, resolveMaxAttempts } from "./lib/structured-output.mjs";
|
||||
import { isLoopbackBind } from "./lib/net.mjs";
|
||||
import { runTuiTurn, reapStaleTuiSessions, resolveTuiHome, bootTuiPane, tuiPaneHealthy, poolPaneName, POOL_BOOT_MS } from "./lib/tui/session.mjs";
|
||||
import { detectTuiUpstreamError } from "./lib/tui/transcript.mjs";
|
||||
@@ -2709,7 +2709,10 @@ async function runStructuredCompletion(upstreamCall, model, messages, conversati
|
||||
continue;
|
||||
}
|
||||
if (structured.mode === "schema" && structured.schema) {
|
||||
const errs = validateJsonSchema(extracted.value, structured.schema, "$", structured.strict);
|
||||
// validateJsonSchemaSafe (#181): a pathologically deep model reply overflows the value-depth
|
||||
// recursion; the safe façade turns that into a validation miss (→ retry → refusal) instead of
|
||||
// a caught RangeError surfacing as a generic 500.
|
||||
const errs = validateJsonSchemaSafe(extracted.value, structured.schema, "$", structured.strict);
|
||||
if (errs.length) {
|
||||
lastErr = "schema validation failed: " + errs.slice(0, 5).join("; ");
|
||||
logEvent("warn", "structured_retry", { attempt, reason: "schema", errors: errs.slice(0, 5) });
|
||||
|
||||
Reference in New Issue
Block a user