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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user