0003 — Prompt content is LF, never CRLF
Status: accepted
Context
Prompt content containing CRLF line endings makes the Azure OpenAI deployment return HTTP 500, deterministically. Not intermittently, not under load — the same content fails every time, and the identical content with LF endings succeeds every time.
Everything about the failure points away from the cause. A 500 from the service reads as their problem. The prompt is valid text either way; nothing rejects it locally, no validation catches it, and the difference is invisible in every editor and in most diffs. It survives a copy-paste between environments intact, so "it works on dv" holds right up until it doesn't.
This is easy to reintroduce without noticing. Pasting a prompt into /Admin/Prompts from a
Windows editor, a git checkout with core.autocrlf=true, or a SQL script saved on Windows all
produce CRLF. The rows seeded by the old HasData migrations (see
0002) still carry it.
Proven with scripts/ai-probe, which reproduces the failure and
delta-debugs the input down to the minimal failing case, producing the apim-request-id evidence
an Azure support case needs.
Decision
Prompt content is stored with LF line endings only, whatever route it takes into the database
— /Admin/Prompts, a direct SQL UPDATE, or a seeded default.
Startup's drift comparison normalises line endings before comparing, so pre-existing CRLF rows do not show up as false drift. That normalisation is for comparison only — it does not rewrite the stored content, and content actually sent to the model is whatever is in the row.
Consequences
- Any tooling that writes prompts must write LF. When editing through SQL, use a client that does
not helpfully convert, and verify with
SELECT CHARINDEX(CHAR(13), Content) FROM Prompts WHERE [Key] = '…'— a non-zero result means a CR is present and that prompt will 500. - A 500 from Azure OpenAI immediately after a prompt edit is this until proven otherwise. Check the line endings before reading anything else.
- The rows still carrying CRLF from the
HasDataera are only safe because nothing has touched them; if one starts failing, this is why. - The real fix is to normalise on write in
/Admin/Promptsand in the seeder, so the invariant is enforced rather than remembered. That has not been done — this record exists because it has not.