From 939b38c5139776ec111e97cb050cd7412efa239d Mon Sep 17 00:00:00 2001 From: FranKaddour Date: Tue, 30 Jun 2026 11:25:01 -0300 Subject: [PATCH 1/3] fix(openai): strip pattern keyword from Responses API schemas to avoid server_error with zod v4 --- .../fix-openai-responses-pattern-keyword.md | 5 +++++ .../openai-responses-language-model.ts | 3 ++- .../openai-responses-prepare-tools.ts | 3 ++- .../openai-responses-sanitize-schema.ts | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-openai-responses-pattern-keyword.md create mode 100644 packages/openai/src/responses/openai-responses-sanitize-schema.ts diff --git a/.changeset/fix-openai-responses-pattern-keyword.md b/.changeset/fix-openai-responses-pattern-keyword.md new file mode 100644 index 000000000000..8592855a4741 --- /dev/null +++ b/.changeset/fix-openai-responses-pattern-keyword.md @@ -0,0 +1,5 @@ +--- +"@ai-sdk/openai": patch +--- + +Fix `server_error` in OpenAI Responses API when schemas generated by zod v4 contain `pattern` keywords with unsupported regex features (e.g. lookaheads from `z.email()`, `z.uuid()`). The `pattern` keyword is now stripped from response format schemas and tool parameters before they are sent to the API. diff --git a/packages/openai/src/responses/openai-responses-language-model.ts b/packages/openai/src/responses/openai-responses-language-model.ts index a4c70054342a..c8241d07cda7 100644 --- a/packages/openai/src/responses/openai-responses-language-model.ts +++ b/packages/openai/src/responses/openai-responses-language-model.ts @@ -1,3 +1,4 @@ +import { removePatternKeyword } from './openai-responses-sanitize-schema'; import { APICallError, type JSONValue, @@ -345,7 +346,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 { strict: strictJsonSchema, name: responseFormat.name ?? 'response', description: responseFormat.description, - schema: responseFormat.schema, + schema: removePatternKeyword(responseFormat.schema), } : { type: 'json_object' }, }), diff --git a/packages/openai/src/responses/openai-responses-prepare-tools.ts b/packages/openai/src/responses/openai-responses-prepare-tools.ts index 0c22453a5a7c..a7f4c8572483 100644 --- a/packages/openai/src/responses/openai-responses-prepare-tools.ts +++ b/packages/openai/src/responses/openai-responses-prepare-tools.ts @@ -23,6 +23,7 @@ import type { OpenAIResponsesFunctionTool, OpenAIResponsesTool, } from './openai-responses-api'; +import { removePatternKeyword } from './openai-responses-sanitize-schema'; type OpenAIToolOptions = { deferLoading?: boolean; @@ -404,7 +405,7 @@ function prepareFunctionTool({ type: 'function', name: tool.name, description: tool.description, - parameters: tool.inputSchema, + parameters: removePatternKeyword(tool.inputSchema), ...(tool.strict != null ? { strict: tool.strict } : {}), ...(deferLoading != null ? { defer_loading: deferLoading } : {}), }; diff --git a/packages/openai/src/responses/openai-responses-sanitize-schema.ts b/packages/openai/src/responses/openai-responses-sanitize-schema.ts new file mode 100644 index 000000000000..996f108efd9e --- /dev/null +++ b/packages/openai/src/responses/openai-responses-sanitize-schema.ts @@ -0,0 +1,19 @@ +// OpenAI structured outputs rejects JSON Schema `pattern` values that contain +// regex features it does not support (e.g. lookaheads). zod v4's toJSONSchema() +// emits `pattern` for string validators like z.email(), z.uuid(), z.iso.date(). +// Strip the keyword recursively before sending schemas to the Responses API. +export function removePatternKeyword(schema: unknown): unknown { + if (!schema || typeof schema !== 'object') return schema; + if (Array.isArray(schema)) return schema.map(removePatternKeyword); + + const result = { ...(schema as Record) }; + delete result['pattern']; + + for (const key of Object.keys(result)) { + if (result[key] && typeof result[key] === 'object') { + result[key] = removePatternKeyword(result[key]); + } + } + + return result; +} From ce27c4a73536bd8bb54587e2b52f7b2dd26fbfd7 Mon Sep 17 00:00:00 2001 From: FranKaddour Date: Tue, 30 Jun 2026 19:14:04 -0300 Subject: [PATCH 2/3] fix(openai): scope removePatternKeyword to JSON Schema structural keys Recurse only through known JSON Schema structural keywords (properties, patternProperties, \, allOf, anyOf, etc.) instead of every object key. Prevents deletion of user-defined tool or response parameters whose name happens to be 'pattern'. --- .../openai-responses-sanitize-schema.ts | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/openai/src/responses/openai-responses-sanitize-schema.ts b/packages/openai/src/responses/openai-responses-sanitize-schema.ts index 996f108efd9e..f0cfb06ce323 100644 --- a/packages/openai/src/responses/openai-responses-sanitize-schema.ts +++ b/packages/openai/src/responses/openai-responses-sanitize-schema.ts @@ -2,16 +2,49 @@ // regex features it does not support (e.g. lookaheads). zod v4's toJSONSchema() // emits `pattern` for string validators like z.email(), z.uuid(), z.iso.date(). // Strip the keyword recursively before sending schemas to the Responses API. +// +// Only standard JSON Schema structural keys are recursed into, so that tool or +// response parameters whose name happens to be "pattern" are never touched. export function removePatternKeyword(schema: unknown): unknown { if (!schema || typeof schema !== 'object') return schema; if (Array.isArray(schema)) return schema.map(removePatternKeyword); const result = { ...(schema as Record) }; + + // Remove the JSON Schema string-validator keyword at this level. delete result['pattern']; - for (const key of Object.keys(result)) { - if (result[key] && typeof result[key] === 'object') { - result[key] = removePatternKeyword(result[key]); + // Keys whose values are { name → schema } maps. Recurse into the schema + // values but preserve the keys — a key named "pattern" here is a user- + // defined property name, not the JSON Schema keyword. + for (const mapKey of ['properties', 'patternProperties', '$defs', 'definitions']) { + if ( + result[mapKey] && + typeof result[mapKey] === 'object' && + !Array.isArray(result[mapKey]) + ) { + result[mapKey] = Object.fromEntries( + Object.entries(result[mapKey] as Record).map( + ([k, v]) => [k, removePatternKeyword(v)], + ), + ); + } + } + + // Keys whose values are sub-schemas or arrays of sub-schemas. + for (const schemaKey of [ + 'items', + 'allOf', + 'anyOf', + 'oneOf', + 'not', + 'additionalProperties', + 'if', + 'then', + 'else', + ]) { + if (result[schemaKey] !== undefined) { + result[schemaKey] = removePatternKeyword(result[schemaKey]); } } From 799beb0c3d4a4ed94e05fc364ebfa1958c370232 Mon Sep 17 00:00:00 2001 From: FranKaddour Date: Wed, 1 Jul 2026 17:03:18 -0300 Subject: [PATCH 3/3] fix(openai): make removePatternKeyword generic to preserve call-site types The function returned unknown, causing a tsc error when assigned to the strongly-typed parameters: JSONSchema7 field of OpenAIResponsesFunctionTool. Using a generic preserves the input type through the return value. --- .../src/responses/openai-responses-sanitize-schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/openai/src/responses/openai-responses-sanitize-schema.ts b/packages/openai/src/responses/openai-responses-sanitize-schema.ts index f0cfb06ce323..0776fbfbe688 100644 --- a/packages/openai/src/responses/openai-responses-sanitize-schema.ts +++ b/packages/openai/src/responses/openai-responses-sanitize-schema.ts @@ -5,9 +5,9 @@ // // Only standard JSON Schema structural keys are recursed into, so that tool or // response parameters whose name happens to be "pattern" are never touched. -export function removePatternKeyword(schema: unknown): unknown { +export function removePatternKeyword(schema: T): T { if (!schema || typeof schema !== 'object') return schema; - if (Array.isArray(schema)) return schema.map(removePatternKeyword); + if (Array.isArray(schema)) return schema.map(removePatternKeyword) as unknown as T; const result = { ...(schema as Record) }; @@ -48,5 +48,5 @@ export function removePatternKeyword(schema: unknown): unknown { } } - return result; + return result as unknown as T; }