Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-invalid-tool-input-object-fallback.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/fix-openai-responses-pattern-keyword.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 6 additions & 2 deletions packages/ai/src/generate-text/parse-tool-call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions packages/ai/src/generate-text/parse-tool-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ export async function parseToolCall<TOOLS extends ToolSet>({
});
}
} 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { removePatternKeyword } from './openai-responses-sanitize-schema';
import {
APICallError,
type JSONValue,
Expand Down Expand Up @@ -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' },
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
OpenAIResponsesFunctionTool,
OpenAIResponsesTool,
} from './openai-responses-api';
import { removePatternKeyword } from './openai-responses-sanitize-schema';

type OpenAIToolOptions = {
deferLoading?: boolean;
Expand Down Expand Up @@ -404,7 +405,7 @@ function prepareFunctionTool({
type: 'function',
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
parameters: removePatternKeyword(tool.inputSchema),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removePatternKeyword returns unknown, which is not assignable to the strongly-typed parameters: JSONSchema7 field of OpenAIResponsesFunctionTool, causing a tsc type error that breaks the build.

Fix on Vercel

...(tool.strict != null ? { strict: tool.strict } : {}),
...(deferLoading != null ? { defer_loading: deferLoading } : {}),
};
Expand Down
52 changes: 52 additions & 0 deletions packages/openai/src/responses/openai-responses-sanitize-schema.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) };

// 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<string, unknown>).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;
}