OpenAI Responses API: in-stream `server_error` triggered by zod v4 string-format regex `pattern`s in tool schemas (gpt-5.4)
Description
Calling streamText with the OpenAI Responses API (gpt-5.4) and tools whose zod v4 schemas use string-format validators (z.email(), z.uuid(), z.iso.date()) can make OpenAI fail with an in-stream server_error immediately after response.in_progress:
{
"type": "error",
"error": {
"type": "server_error",
"code": "server_error",
"message": "An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com...",
"param": null
},
"sequence_number": 2
}The trigger is the regex pattern keywords that zod v4's toJSONSchema() emits for these validators (and which the AI SDK forwards to the provider verbatim):
// z.email() — contains negative lookaheads, which OpenAI's structured-output
// docs list as unsupported regex features
"email": {
"type": "string",
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
// z.iso.date() — large leap-year-aware alternation
"birthDate": {
"type": "string",
"format": "date",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
}
// z.uuid()
"kidId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"
}Evidence (replaying the captured /v1/responses body directly against api.openai.com)
| Variant | Result |
|---|---|
| Captured body (12 tools, ~3.5k-token system prompt, 5-message history) | 24/24 in-stream server_error over several hours |
Identical body, only pattern keywords deleted from tool schemas |
0 failures |
Identical body, pattern deleted but format kept |
0 failures (format alone is fine) |
| Small perturbations (different last user message, one tool dropped, random prefix added) | flaky, ~40–70% failure |
| Each suspect schema in isolation (1 tool, short prompt) | passes |
So the patterns are necessary but not sufficient — failure probability scales with aggregate prompt/schema complexity, and for some real conversations it reaches 100%. In production this presents as "one conversation thread permanently broken while others work," which is nasty to diagnose because the error is a generic 500 with no param pointer.
The failure arrives ~0.7–1.2s into the stream, before any output item, suggesting OpenAI dies preparing constrained decoding/grammar state rather than during sampling. (The Responses API now defaults function tools to strict: true.)
I could not produce a shareable always-failing repro — the failing manifold is token-sensitive, and anonymizing the conversation content changes the failure rate (my fully genericized rewrite passes consistently). I'm happy to share the exact failing body privately. Failing OpenAI request IDs that OpenAI can inspect server-side:
req_e2157565c8594b388f30971ed6b0b6ab, req_ec60e77c95da4f46b20c397e9758d9ae, req_5f484d89e2d7498f89a6806f8f59b85d, req_496dd2490bea4171a06605a0bcb6ce4d, req_bdee906f91394404b36c4b4becb1d4a9, req_c93d169ddb654e8ebc46d641150470f5, req_427dac0fb0d3425687fe1068e2b737c6, req_e667f935118a4d2386dba73e3cb6ddd6
This is arguably an OpenAI backend bug — why file here?
- The AI SDK's default zod v4 conversion is what puts these regexes (including lookaheads OpenAI documents as unsupported) into every tool schema, so AI SDK users hit this without ever writing a
patternthemselves. The provider could sanitizepattern(or at least strip known-unsupported regex features) for OpenAI, the way other provider-specific schema fixes are handled. - Failing that, a troubleshooting docs entry would help — this is a close cousin of the existing "
.optional()/.nullish()break OpenAI structured outputs" page, but it presents as a retryable-looking 500 instead of a clean error, so people burn a lot of time before suspecting their schemas.
Workaround
Provider-level middleware that strips pattern keywords before the request, while zod still validates tool inputs at runtime:
import { wrapLanguageModel, type LanguageModelMiddleware } from "ai";
const stripJsonSchemaPatternsMiddleware: LanguageModelMiddleware = {
specificationVersion: "v3",
transformParams: async ({ params }) => ({
...params,
tools: params.tools?.map(tool =>
tool.type === "function"
? { ...tool, inputSchema: stripPatternKeywordDeep(tool.inputSchema) }
: tool
),
responseFormat:
params.responseFormat?.type === "json" && params.responseFormat.schema
? {
...params.responseFormat,
schema: stripPatternKeywordDeep(params.responseFormat.schema),
}
: params.responseFormat,
}),
};
function stripPatternKeywordDeep<T>(schema: T): T {
const visit = (node: unknown): unknown => {
if (Array.isArray(node)) return node.map(visit);
if (node === null || typeof node !== "object") return node;
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(node)) {
if (key === "pattern" && typeof value === "string") continue;
// keys of these maps are property names, not schema keywords
if (
["properties", "patternProperties", "$defs", "definitions"].includes(key) &&
value && typeof value === "object" && !Array.isArray(value)
) {
out[key] = Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, visit(v)])
);
continue;
}
out[key] = visit(value);
}
return out;
};
return visit(schema) as T;
}
const model = wrapLanguageModel({
model: openai("gpt-5.4"),
middleware: stripJsonSchemaPatternsMiddleware,
});With this middleware the previously 100%-failing conversation streams fine (verified repeatedly).
AI SDK Version
ai: 6.0.39@ai-sdk/openai: 3.0.12zod: 3.25.76 (schemas authored viaimport { z } from "zod/v4")- Model:
gpt-5.4(Responses API, streaming), reproduced both via a gateway (Portkey) and directly againstapi.openai.com
Source: vercel/ai