feat(preview): log spec validation failures with raw output
All checks were successful
Deploy to Production / deploy (push) Successful in 1m25s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m25s
422s from /preview hid the actual reason: zod_message tells which field was wrong and a 400-char preview of the model output reveals refusals or non-JSON returns. Both stay in the api log only — never surfaced to the client unchanged.
This commit is contained in:
parent
5a8e736113
commit
979d1abfca
@ -121,9 +121,30 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof SpecValidationError) {
|
if (err instanceof SpecValidationError) {
|
||||||
|
// Log the actual Zod validation failure so we can see *which* field
|
||||||
|
// the model got wrong. Without this we can only see "422" in the
|
||||||
|
// logs and can't tell whether to fix the prompt, the schema, or
|
||||||
|
// the model output cleanup. Prompt is truncated to keep PII risk
|
||||||
|
// bounded.
|
||||||
|
app.log.warn(
|
||||||
|
{
|
||||||
|
zod_message: err.message,
|
||||||
|
prompt: parsed.data.prompt.slice(0, 200),
|
||||||
|
model: choice.displayName,
|
||||||
|
},
|
||||||
|
'preview_spec_invalid',
|
||||||
|
);
|
||||||
return reply.code(422).send({ error: 'spec_invalid', detail: err.message });
|
return reply.code(422).send({ error: 'spec_invalid', detail: err.message });
|
||||||
}
|
}
|
||||||
if (err instanceof BannedPatternError) {
|
if (err instanceof BannedPatternError) {
|
||||||
|
app.log.warn(
|
||||||
|
{
|
||||||
|
reason: err.message,
|
||||||
|
prompt: parsed.data.prompt.slice(0, 200),
|
||||||
|
model: choice.displayName,
|
||||||
|
},
|
||||||
|
'preview_banned_pattern',
|
||||||
|
);
|
||||||
return reply.code(422).send({ error: 'banned_pattern', detail: err.message });
|
return reply.code(422).send({ error: 'banned_pattern', detail: err.message });
|
||||||
}
|
}
|
||||||
if (err instanceof SpecTimeoutError) {
|
if (err instanceof SpecTimeoutError) {
|
||||||
|
|||||||
@ -268,7 +268,13 @@ async function generateWithAnthropic(
|
|||||||
.join('');
|
.join('');
|
||||||
const json = extractJson(text);
|
const json = extractJson(text);
|
||||||
const parsed = GeneratorSpec.safeParse(json);
|
const parsed = GeneratorSpec.safeParse(json);
|
||||||
if (!parsed.success) throw new SpecValidationError(parsed.error.message);
|
if (!parsed.success) {
|
||||||
|
// Include a truncated raw preview so the caller (api log) can see whether
|
||||||
|
// the model returned non-JSON / a refusal / a near-miss schema, instead
|
||||||
|
// of just the opaque zod error.
|
||||||
|
const preview = text.slice(0, 400).replace(/\s+/g, ' ');
|
||||||
|
throw new SpecValidationError(`${parsed.error.message} :: raw="${preview}"`);
|
||||||
|
}
|
||||||
scanForInjection(parsed.data);
|
scanForInjection(parsed.data);
|
||||||
return { spec: parsed.data, source: 'claude' };
|
return { spec: parsed.data, source: 'claude' };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user