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
This commit is contained in:
roboomp
2026-06-16 15:24:54 +00:00
parent dc14689fc1
commit a93a6c3908
4 changed files with 78 additions and 0 deletions
+4
View File
@@ -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
+24
View File
@@ -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(["=", "!", "<=", "<!"]);
function hasOpenAIUnsupportedRegexLookaround(pattern: string): boolean {
let groupStart = pattern.indexOf("(?");
while (groupStart !== -1) {
let escapes = 0;
for (let i = groupStart - 1; i >= 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<JsonObject, JsonObject>): unknown {
if (!isJsonObject(value)) return value;
@@ -973,6 +989,14 @@ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap<JsonO
changed = true;
continue;
}
if (
key === "pattern" &&
typeof value.pattern === "string" &&
hasOpenAIUnsupportedRegexLookaround(value.pattern)
) {
changed = true;
continue;
}
const child = value[key];
let next: unknown = child;
+28
View File
@@ -41,6 +41,34 @@ describe("openai-codex tool schemas", () => {
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", () => {
@@ -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<string, unknown>;