diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index daf0d1c79..104a1ba4c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI Responses/Codex tool schema normalization stripping provider-rejected regex lookaround patterns from MCP tool parameter schemas. ([#2784](https://github.com/can1357/oh-my-pi/issues/2784)) + ## [16.0.2] - 2026-06-16 ### Added diff --git a/packages/ai/src/utils/schema/normalize.ts b/packages/ai/src/utils/schema/normalize.ts index 7d16b7ebd..32e3791f3 100644 --- a/packages/ai/src/utils/schema/normalize.ts +++ b/packages/ai/src/utils/schema/normalize.ts @@ -936,6 +936,22 @@ export function sanitizeSchemaForOpenAIResponses(schema: JsonObject): JsonObject * `normalizeSchemaFor*` dispatcher naming used elsewhere in this module. */ export const normalizeSchemaForOpenAIResponses: (schema: JsonObject) => JsonObject = sanitizeSchemaForOpenAIResponses; +const OPENAI_UNSUPPORTED_REGEX_LOOKAROUNDS = new Set(["=", "!", "<=", "= 0 && pattern[i] === "\\"; i--) escapes++; + if (escapes % 2 === 0) { + const operator = + pattern[groupStart + 2] === "<" ? pattern.slice(groupStart + 2, groupStart + 4) : pattern[groupStart + 2]; + if (OPENAI_UNSUPPORTED_REGEX_LOOKAROUNDS.has(operator)) return true; + } + groupStart = pattern.indexOf("(?", groupStart + 2); + } + return false; +} function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap): unknown { if (!isJsonObject(value)) return value; @@ -973,6 +989,14 @@ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap { parameters: { type: "object", properties: {} }, }); }); + it("strips MCP regex lookaround patterns from function parameters", () => { + const tools: Tool[] = [ + { + name: "get_design_context", + description: "Get Figma design context", + parameters: { + type: "object", + properties: { + fileKey: { type: "string", pattern: "^(?!undefined$|null$)" }, + }, + }, + }, + ]; + + const converted = convertOpenAICodexResponsesTools(tools, createCodexModel("gpt-5.5")); + + expect(converted[0]).toEqual({ + type: "function", + name: "get_design_context", + description: "Get Figma design context", + parameters: { + type: "object", + properties: { + fileKey: { type: "string" }, + }, + }, + }); + }); }); describe("openai-codex request transformer", () => { diff --git a/packages/ai/test/schema-normalization.test.ts b/packages/ai/test/schema-normalization.test.ts index caa79a80d..3093f9e5d 100644 --- a/packages/ai/test/schema-normalization.test.ts +++ b/packages/ai/test/schema-normalization.test.ts @@ -519,6 +519,28 @@ describe("sanitizeSchemaForOpenAIResponses", () => { }); }); + it("strips regex lookaround patterns unsupported by OpenAI Responses", () => { + const schema = { + type: "object", + properties: { + fileKey: { type: "string", pattern: "^(?!undefined$|null$)" }, + ending: { type: "string", pattern: "(?<=/)node$" }, + slug: { type: "string", pattern: "^[a-z0-9_-]+$" }, + literal: { type: "string", pattern: "\\(?!literal" }, + }, + }; + + expect(sanitizeSchemaForOpenAIResponses(schema)).toEqual({ + type: "object", + properties: { + fileKey: { type: "string" }, + ending: { type: "string" }, + slug: { type: "string", pattern: "^[a-z0-9_-]+$" }, + literal: { type: "string", pattern: "\\(?!literal" }, + }, + }); + }); + it("preserves non-array oneOf payloads verbatim instead of dropping them", () => { const malformed = { type: "object", oneOf: { type: "object" } } as unknown as Record;