diff --git a/.changeset/fix-invalid-tool-input-object-fallback.md b/.changeset/fix-invalid-tool-input-object-fallback.md new file mode 100644 index 000000000000..8e511556de00 --- /dev/null +++ b/.changeset/fix-invalid-tool-input-object-fallback.md @@ -0,0 +1,5 @@ +--- +"ai": patch +--- + +Fix invalid tool call input being stored as a raw string in multi-turn conversations. When a tool call contains JSON that cannot be parsed, the input is now wrapped as `{ rawInvalidInput: string }` instead of storing the raw string directly. This prevents providers such as Amazon Bedrock from rejecting the conversation history on the next turn with a format error. 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/ai/src/generate-text/parse-tool-call.test.ts b/packages/ai/src/generate-text/parse-tool-call.test.ts index 6fae09532489..dd619c478d73 100644 --- a/packages/ai/src/generate-text/parse-tool-call.test.ts +++ b/packages/ai/src/generate-text/parse-tool-call.test.ts @@ -471,7 +471,9 @@ describe('parseToolCall', () => { "dynamic": true, "error": [AI_InvalidToolInputError: Invalid input for tool testTool: AI_JSONParseError: JSON parsing failed: Text: invalid json. Error message: SyntaxError: Unexpected token 'i', "invalid json" is not valid JSON], - "input": "invalid json", + "input": { + "rawInvalidInput": "invalid json", + }, "invalid": true, "providerExecuted": undefined, "providerMetadata": undefined, @@ -510,7 +512,9 @@ describe('parseToolCall', () => { { "dynamic": true, "error": [AI_ToolCallRepairError: Error repairing tool call: Error: test error], - "input": "invalid json", + "input": { + "rawInvalidInput": "invalid json", + }, "invalid": true, "providerExecuted": undefined, "providerMetadata": undefined, diff --git a/packages/ai/src/generate-text/parse-tool-call.ts b/packages/ai/src/generate-text/parse-tool-call.ts index 5066b1085a1e..c11046ebd06f 100644 --- a/packages/ai/src/generate-text/parse-tool-call.ts +++ b/packages/ai/src/generate-text/parse-tool-call.ts @@ -92,9 +92,11 @@ export async function parseToolCall({ }); } } catch (error) { - // use parsed input when possible + // use parsed input when possible; if the raw input is not valid JSON wrap it + // in an object so downstream providers (e.g. Bedrock) always receive a JSON + // object for toolUse.input rather than a bare string that they would reject. const parsedInput = await safeParseJSON({ text: toolCall.input }); - const input = parsedInput.success ? parsedInput.value : toolCall.input; + const input = parsedInput.success ? parsedInput.value : { rawInvalidInput: toolCall.input }; const tool = tools?.[toolCall.toolName]; // TODO AI SDK 6: special invalid tool call parts 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..f0cfb06ce323 --- /dev/null +++ b/packages/openai/src/responses/openai-responses-sanitize-schema.ts @@ -0,0 +1,52 @@ +// 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. +// +// 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']; + + // 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]); + } + } + + return result; +}