@ai2070/l0
Version:
L0: The Missing Reliability Substrate for AI
440 lines (439 loc) • 13.4 kB
JavaScript
import { z } from "zod";
import { l0 } from "./runtime/l0";
import { autoCorrectJSON, isValidJSON, extractJSON } from "./utils/autoCorrect";
async function structured(options) {
const {
schema,
stream: streamFactory,
fallbackStreams = [],
retry = {},
autoCorrect = true,
strictMode = false,
timeout,
signal,
monitoring,
detectZeroTokens,
onValidationError,
onAutoCorrect,
onRetry
} = options;
let validationAttempts = 0;
let validationFailures = 0;
let autoCorrections = 0;
const correctionTypes = [];
const validationErrors = [];
let rawOutput = "";
let appliedCorrections = [];
let wasAutoCorrected = false;
const errors = [];
let validationStartTime = 0;
let validationEndTime = 0;
const abortController = new AbortController();
let parsedData = null;
const tryParseAndValidate = (content) => {
validationAttempts++;
validationStartTime = Date.now();
let processed = content;
appliedCorrections = [];
if (autoCorrect) {
const correctionResult = autoCorrectJSON(processed, {
structural: true,
stripFormatting: true,
schemaBased: false,
strict: strictMode
});
if (correctionResult.corrections.length > 0) {
wasAutoCorrected = true;
processed = correctionResult.corrected;
appliedCorrections = correctionResult.corrections;
autoCorrections++;
correctionTypes.push(...correctionResult.corrections);
if (onAutoCorrect) {
const correctionInfo = {
original: content,
corrected: processed,
corrections: correctionResult.corrections,
success: correctionResult.success
};
onAutoCorrect(correctionInfo);
}
}
}
let parsed;
try {
parsed = JSON.parse(processed);
} catch (parseError) {
const err = parseError instanceof Error ? parseError : new Error(String(parseError));
const extracted = extractJSON(processed);
if (extracted !== processed) {
try {
parsed = JSON.parse(extracted);
wasAutoCorrected = true;
if (!appliedCorrections.includes("extract_json")) {
appliedCorrections.push("extract_json");
correctionTypes.push("extract_json");
}
autoCorrections++;
} catch {
const rescueResult = autoCorrectJSON(extracted, {
structural: true,
stripFormatting: true
});
if (rescueResult.success) {
try {
parsed = JSON.parse(rescueResult.corrected);
wasAutoCorrected = true;
appliedCorrections.push(...rescueResult.corrections);
autoCorrections++;
correctionTypes.push(...rescueResult.corrections);
} catch (innerErr) {
const innerError = innerErr instanceof Error ? innerErr : new Error(String(innerErr));
return {
success: false,
error: `Invalid JSON after auto-correction: ${innerError.message}`
};
}
} else {
return {
success: false,
error: `Invalid JSON after auto-correction: ${err.message}`
};
}
}
} else {
const rawExtracted = extractJSON(content);
if (rawExtracted !== content) {
const rescueResult = autoCorrectJSON(rawExtracted, {
structural: true,
stripFormatting: true
});
if (rescueResult.success) {
try {
parsed = JSON.parse(rescueResult.corrected);
wasAutoCorrected = true;
appliedCorrections.push(
"extract_json",
...rescueResult.corrections
);
autoCorrections++;
correctionTypes.push("extract_json", ...rescueResult.corrections);
} catch (innerErr) {
const innerError = innerErr instanceof Error ? innerErr : new Error(String(innerErr));
return {
success: false,
error: `Invalid JSON: ${innerError.message}`
};
}
} else {
return { success: false, error: `Invalid JSON: ${err.message}` };
}
} else {
return { success: false, error: `Invalid JSON: ${err.message}` };
}
}
}
const validationResult = schema.safeParse(parsed);
if (!validationResult.success) {
validationFailures++;
validationErrors.push(validationResult.error);
if (onValidationError) {
onValidationError(validationResult.error, validationAttempts);
}
return {
success: false,
error: `Schema validation failed: ${validationResult.error.errors[0]?.message}`
};
}
validationEndTime = Date.now();
parsedData = validationResult.data;
return { success: true, data: validationResult.data };
};
const l0Options = {
stream: streamFactory,
fallbackStreams,
retry: {
attempts: retry.attempts ?? 2,
backoff: retry.backoff ?? "fixed-jitter",
baseDelay: retry.baseDelay ?? 1e3,
maxDelay: retry.maxDelay ?? 5e3,
retryOn: [...retry.retryOn || [], "guardrail_violation", "incomplete"],
errorTypeDelays: retry.errorTypeDelays
},
timeout,
signal: signal || abortController.signal,
// Default to disabled for structured output since short valid JSON
// (like "[]" or "{}") should not be rejected
detectZeroTokens: detectZeroTokens ?? false,
monitoring: {
enabled: monitoring?.enabled ?? false,
sampleRate: monitoring?.sampleRate ?? 1,
metadata: {
...monitoring?.metadata || {},
structured: true,
schemaName: schema.description || "unknown"
}
},
guardrails: [
// JSON + Schema validation guardrail (runs on completion)
{
name: "json-schema-validation",
check: (context) => {
if (!context.completed) {
return [];
}
if (!isValidJSON(context.content)) {
return [
{
rule: "json-schema-validation",
message: "Output is not valid JSON",
severity: "error",
recoverable: true
}
];
}
const result2 = tryParseAndValidate(context.content);
if (!result2.success) {
return [
{
rule: "json-schema-validation",
message: result2.error || "Validation failed",
severity: "error",
recoverable: true
}
];
}
return [];
}
}
],
onRetry: (attempt, reason) => {
if (onRetry) {
onRetry(attempt, reason);
}
}
};
const result = await l0(l0Options);
rawOutput = "";
for await (const event of result.stream) {
if (event.type === "token" && event.value) {
rawOutput += event.value;
} else if (event.type === "error") {
errors.push(event.error || new Error("Unknown error"));
}
}
if (!rawOutput || rawOutput.trim().length === 0) {
throw new Error("No output received from model");
}
if (parsedData === null) {
const finalResult = tryParseAndValidate(rawOutput);
if (!finalResult.success) {
throw new Error(`Structured output failed: ${finalResult.error}`);
}
}
const structuredState = {
...result.state,
validationFailures,
autoCorrections,
validationErrors
};
let structuredTelemetry;
if (result.telemetry) {
structuredTelemetry = {
...result.telemetry,
structured: {
schemaName: schema.description || "unknown",
validationAttempts,
validationFailures,
autoCorrections,
correctionTypes: Array.from(new Set(correctionTypes)),
validationSuccess: true,
validationTime: validationEndTime - validationStartTime
}
};
}
return {
data: parsedData,
raw: rawOutput,
corrected: wasAutoCorrected,
corrections: appliedCorrections,
state: structuredState,
telemetry: structuredTelemetry,
errors,
abort: () => abortController.abort()
};
}
async function structuredObject(shape, options) {
const schema = z.object(shape);
return structured({ ...options, schema });
}
async function structuredArray(itemSchema, options) {
const schema = z.array(itemSchema);
return structured({ ...options, schema });
}
async function structuredStream(options) {
const {
schema,
stream: streamFactory,
fallbackStreams = [],
retry = {},
autoCorrect = true,
strictMode = false,
detectZeroTokens,
timeout,
signal,
monitoring,
onValidationError,
onAutoCorrect,
onRetry
} = options;
const abortController = new AbortController();
const combinedSignal = signal || abortController.signal;
let rawOutput = "";
let validationAttempts = 0;
let validationFailures = 0;
let autoCorrections = 0;
const correctionTypes = [];
const validationErrors = [];
let appliedCorrections = [];
let wasAutoCorrected = false;
const errors = [];
const l0Result = await l0({
stream: streamFactory,
fallbackStreams,
retry: {
attempts: retry.attempts ?? 2,
backoff: retry.backoff ?? "fixed-jitter",
baseDelay: retry.baseDelay ?? 1e3,
maxDelay: retry.maxDelay ?? 5e3,
retryOn: [...retry.retryOn || [], "guardrail_violation", "incomplete"],
errorTypeDelays: retry.errorTypeDelays
},
detectZeroTokens: detectZeroTokens ?? false,
timeout,
signal: combinedSignal,
monitoring: {
enabled: monitoring?.enabled ?? false,
sampleRate: monitoring?.sampleRate ?? 1,
metadata: {
...monitoring?.metadata || {},
structured: true,
schemaName: schema.description || "unknown"
}
},
onRetry
});
let streamResolve;
const streamDone = new Promise((resolve) => {
streamResolve = resolve;
});
const teedStream = async function* () {
for await (const event of l0Result.stream) {
if (event.type === "token" && event.value) {
rawOutput += event.value;
} else if (event.type === "error") {
errors.push(event.error || new Error("Unknown error"));
}
yield event;
}
streamResolve();
};
const sharedStream = teedStream();
const resultPromise = (async () => {
await streamDone;
if (!rawOutput || rawOutput.trim().length === 0) {
throw new Error("No output received from model");
}
let processed = rawOutput;
appliedCorrections = [];
if (autoCorrect) {
const correctionResult = autoCorrectJSON(processed, {
structural: true,
stripFormatting: true,
schemaBased: false,
strict: strictMode
});
if (correctionResult.corrections.length > 0) {
wasAutoCorrected = true;
processed = correctionResult.corrected;
appliedCorrections = correctionResult.corrections;
autoCorrections++;
correctionTypes.push(...correctionResult.corrections);
if (onAutoCorrect) {
onAutoCorrect({
original: rawOutput,
corrected: processed,
corrections: correctionResult.corrections,
success: correctionResult.success
});
}
}
}
let parsedData;
try {
parsedData = JSON.parse(processed);
} catch (parseError) {
const extracted = extractJSON(processed);
if (extracted !== processed) {
try {
parsedData = JSON.parse(extracted);
wasAutoCorrected = true;
appliedCorrections.push("extract_json");
} catch {
throw new Error(`Invalid JSON: ${parseError}`);
}
} else {
throw new Error(`Invalid JSON: ${parseError}`);
}
}
validationAttempts++;
const validationResult = schema.safeParse(parsedData);
if (!validationResult.success) {
validationFailures++;
validationErrors.push(validationResult.error);
if (onValidationError) {
onValidationError(validationResult.error, validationAttempts);
}
throw new Error(
`Schema validation failed: ${validationResult.error.errors[0]?.message}`
);
}
const structuredState = {
...l0Result.state,
validationFailures,
autoCorrections,
validationErrors
};
return {
data: validationResult.data,
raw: rawOutput,
corrected: wasAutoCorrected,
corrections: appliedCorrections,
state: structuredState,
telemetry: l0Result.telemetry ? {
...l0Result.telemetry,
structured: {
schemaName: schema.description || "unknown",
validationAttempts,
validationFailures,
autoCorrections,
correctionTypes: Array.from(new Set(correctionTypes)),
validationSuccess: true
}
} : void 0,
errors,
abort: () => abortController.abort()
};
})();
return {
stream: sharedStream,
result: resultPromise,
abort: () => abortController.abort()
};
}
export {
structured,
structuredArray,
structuredObject,
structuredStream
};
//# sourceMappingURL=structured.js.map