From a93a6c39085cac8a289f82237067bbfc1bd6bdb8 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 16 Jun 2026 15:24:54 +0000 Subject: [PATCH 1/3] fix(ai): stripped openai responses regex lookarounds Removed JSON Schema pattern values containing regex lookaround from OpenAI Responses/Codex tool schemas so incompatible MCP tools do not poison the request. Added schema-normalization and Codex conversion regression coverage for Figma-style fileKey patterns. Fixes #2784 --- packages/ai/CHANGELOG.md | 4 +++ packages/ai/src/utils/schema/normalize.ts | 24 ++++++++++++++++ packages/ai/test/openai-codex.test.ts | 28 +++++++++++++++++++ packages/ai/test/schema-normalization.test.ts | 22 +++++++++++++++ 4 files changed, 78 insertions(+) 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; From 965afbe3779cc887e695c20a61e6d3075ccb34a1 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 16 Jun 2026 15:42:32 +0000 Subject: [PATCH 2/3] fix(ai): stripped openai pattern property lookarounds Dropped unsupported lookaround regex keys from OpenAI Responses patternProperties maps while preserving ordinary property names and supported regex keys. Extended sanitizer and Codex tool conversion coverage for patternProperties. Fixes #2784 --- packages/ai/src/utils/schema/normalize.ts | 16 ++++++++-- packages/ai/test/openai-codex.test.ts | 30 +++++++++++++++++++ packages/ai/test/schema-normalization.test.ts | 12 ++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/ai/src/utils/schema/normalize.ts b/packages/ai/src/utils/schema/normalize.ts index 32e3791f3..89a4c3db8 100644 --- a/packages/ai/src/utils/schema/normalize.ts +++ b/packages/ai/src/utils/schema/normalize.ts @@ -1000,8 +1000,10 @@ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap): JsonObject { +function normalizeOpenAIResponsesSchemaMap( + schemaMap: JsonObject, + cache: WeakMap, + stripUnsupportedRegexKeys: boolean, +): JsonObject { let changed = false; const output: JsonObject = {}; for (const key in schemaMap) { if (!Object.hasOwn(schemaMap, key)) continue; + if (stripUnsupportedRegexKeys && hasOpenAIUnsupportedRegexLookaround(key)) { + changed = true; + continue; + } const child = schemaMap[key]; const next = normalizeOpenAIResponsesSchemaNode(child, cache); if (next !== child) changed = true; diff --git a/packages/ai/test/openai-codex.test.ts b/packages/ai/test/openai-codex.test.ts index 76a1b708b..281fb0be6 100644 --- a/packages/ai/test/openai-codex.test.ts +++ b/packages/ai/test/openai-codex.test.ts @@ -69,6 +69,36 @@ describe("openai-codex tool schemas", () => { }, }); }); + it("strips MCP regex lookaround patternProperties from function parameters", () => { + const tools: Tool[] = [ + { + name: "read_dynamic_values", + description: "Read dynamic values", + parameters: { + type: "object", + patternProperties: { + "^(?!secret_)": { type: "string" }, + "^public_": { type: "string" }, + }, + }, + }, + ]; + + const converted = convertOpenAICodexResponsesTools(tools, createCodexModel("gpt-5.5")); + + expect(converted[0]).toEqual({ + type: "function", + name: "read_dynamic_values", + description: "Read dynamic values", + parameters: { + type: "object", + patternProperties: { + "^public_": { type: "string" }, + }, + properties: {}, + }, + }); + }); }); describe("openai-codex request transformer", () => { diff --git a/packages/ai/test/schema-normalization.test.ts b/packages/ai/test/schema-normalization.test.ts index 3093f9e5d..35c3a5720 100644 --- a/packages/ai/test/schema-normalization.test.ts +++ b/packages/ai/test/schema-normalization.test.ts @@ -527,6 +527,13 @@ describe("sanitizeSchemaForOpenAIResponses", () => { ending: { type: "string", pattern: "(?<=/)node$" }, slug: { type: "string", pattern: "^[a-z0-9_-]+$" }, literal: { type: "string", pattern: "\\(?!literal" }, + "^(?!property-name)": { type: "string" }, + }, + patternProperties: { + "^(?!secret_)": { type: "string" }, + "(?<=/)node$": { type: "string" }, + "^x-": { type: "object" }, + "\\(?!literal": { type: "string" }, }, }; @@ -537,6 +544,11 @@ describe("sanitizeSchemaForOpenAIResponses", () => { ending: { type: "string" }, slug: { type: "string", pattern: "^[a-z0-9_-]+$" }, literal: { type: "string", pattern: "\\(?!literal" }, + "^(?!property-name)": { type: "string" }, + }, + patternProperties: { + "^x-": { type: "object", properties: {} }, + "\\(?!literal": { type: "string" }, }, }); }); From e4444a0fe7b2f68719ad2de0a3965e5252e24516 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 16 Jun 2026 15:54:50 +0000 Subject: [PATCH 3/3] fix(ai): normalized stripped openai schemas Converted schema nodes emptied by OpenAI Responses lookaround stripping to boolean true so pattern-only nodes keep the existing empty-schema semantics. Extended sanitizer and Codex regression coverage for pattern-only property and propertyNames schemas. Fixes #2784 --- packages/ai/src/utils/schema/normalize.ts | 8 ++++---- packages/ai/test/openai-codex.test.ts | 2 ++ packages/ai/test/schema-normalization.test.ts | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/utils/schema/normalize.ts b/packages/ai/src/utils/schema/normalize.ts index 89a4c3db8..d65c1c0ae 100644 --- a/packages/ai/src/utils/schema/normalize.ts +++ b/packages/ai/src/utils/schema/normalize.ts @@ -953,7 +953,7 @@ function hasOpenAIUnsupportedRegexLookaround(pattern: string): boolean { return false; } -function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap): unknown { +function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap): unknown { if (!isJsonObject(value)) return value; // `{}` (empty JSON Schema) ≡ `true` (JSON Schema draft 2020-12 §4.3.1). @@ -1034,7 +1034,7 @@ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap): unknown[] { +function normalizeOpenAIResponsesSchemaArray(value: unknown[], cache: WeakMap): unknown[] { let changed = false; const output = value.map(item => { const next = normalizeOpenAIResponsesSchemaNode(item, cache); @@ -1060,7 +1060,7 @@ function normalizeOpenAIResponsesSchemaArray(value: unknown[], cache: WeakMap, + cache: WeakMap, stripUnsupportedRegexKeys: boolean, ): JsonObject { let changed = false; diff --git a/packages/ai/test/openai-codex.test.ts b/packages/ai/test/openai-codex.test.ts index 281fb0be6..d5a8aa680 100644 --- a/packages/ai/test/openai-codex.test.ts +++ b/packages/ai/test/openai-codex.test.ts @@ -51,6 +51,7 @@ describe("openai-codex tool schemas", () => { properties: { fileKey: { type: "string", pattern: "^(?!undefined$|null$)" }, }, + propertyNames: { pattern: "^(?!undefined$|null$)" }, }, }, ]; @@ -66,6 +67,7 @@ describe("openai-codex tool schemas", () => { properties: { fileKey: { type: "string" }, }, + propertyNames: true, }, }); }); diff --git a/packages/ai/test/schema-normalization.test.ts b/packages/ai/test/schema-normalization.test.ts index 35c3a5720..de62e5ea6 100644 --- a/packages/ai/test/schema-normalization.test.ts +++ b/packages/ai/test/schema-normalization.test.ts @@ -527,6 +527,7 @@ describe("sanitizeSchemaForOpenAIResponses", () => { ending: { type: "string", pattern: "(?<=/)node$" }, slug: { type: "string", pattern: "^[a-z0-9_-]+$" }, literal: { type: "string", pattern: "\\(?!literal" }, + patternOnly: { pattern: "^(?!bad$)" }, "^(?!property-name)": { type: "string" }, }, patternProperties: { @@ -535,6 +536,7 @@ describe("sanitizeSchemaForOpenAIResponses", () => { "^x-": { type: "object" }, "\\(?!literal": { type: "string" }, }, + propertyNames: { pattern: "^(?!invalid$)" }, }; expect(sanitizeSchemaForOpenAIResponses(schema)).toEqual({ @@ -544,12 +546,14 @@ describe("sanitizeSchemaForOpenAIResponses", () => { ending: { type: "string" }, slug: { type: "string", pattern: "^[a-z0-9_-]+$" }, literal: { type: "string", pattern: "\\(?!literal" }, + patternOnly: true, "^(?!property-name)": { type: "string" }, }, patternProperties: { "^x-": { type: "object", properties: {} }, "\\(?!literal": { type: "string" }, }, + propertyNames: true, }); });