diff --git a/apps/api/src/routes/servers.ts b/apps/api/src/routes/servers.ts index f6dedf2..bc0ae41 100644 --- a/apps/api/src/routes/servers.ts +++ b/apps/api/src/routes/servers.ts @@ -121,9 +121,30 @@ export async function serverRoutes(app: FastifyInstance): Promise { }); } catch (err) { 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 }); } 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 }); } if (err instanceof SpecTimeoutError) { diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 8c5a874..0711440 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -268,7 +268,13 @@ async function generateWithAnthropic( .join(''); const json = extractJson(text); 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); return { spec: parsed.data, source: 'claude' }; }