fix(ai): flattened xAI exclusive-required anyOf at tool root only
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
- Stopped runaway exact response cycles across model providers and kept persistent loops fail-closed after bounded guarded retries ([#8669](https://github.com/can1357/oh-my-pi/pull/8669) by [@pstarkgit](https://github.com/pstarkgit)).
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Responses / Completions and xAI tool conversion 400ing the whole turn on MCP schemas whose root is an object plus a typeless exclusive-required `anyOf` (e.g. codebase-memory `check_index_coverage`). Those fragments now flatten in `toolWireSchema` so every other tool stays callable.
|
||||
- Fixed OpenAI Responses / Completions and xAI tool conversion 400ing the whole turn on MCP schemas whose root is an object plus a typeless exclusive-required `anyOf` (e.g. codebase-memory `check_index_coverage`). Flatten only the **tool root** — nested unions (e.g. `task.outputSchema`) stay intact so Grok still sees valid `task`/`edit` schemas.
|
||||
|
||||
|
||||
## [17.3.4] - 2026-08-14
|
||||
|
||||
@@ -1472,7 +1472,11 @@ const OPENAI_RESPONSES_SCHEMA_VALUE_KEYS = new Set([
|
||||
* would not survive).
|
||||
*/
|
||||
export function sanitizeSchemaForOpenAIResponses(schema: JsonObject): JsonObject {
|
||||
return normalizeOpenAIResponsesSchemaNode(schema, new WeakMap()) as JsonObject;
|
||||
const sanitized = normalizeOpenAIResponsesSchemaNode(schema, new WeakMap()) as JsonObject;
|
||||
if (isJsonObject(sanitized)) {
|
||||
flattenOpenAIResponsesObjectConstraintUnion(sanitized);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1575,10 +1579,6 @@ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap<JsonO
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (flattenOpenAIResponsesObjectConstraintUnion(output)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
||||
// Safe to overwrite the seed: any cyclic re-entry above already observed
|
||||
// the seeded partial and set `changed = true` for that node, so a node
|
||||
|
||||
@@ -114,6 +114,9 @@ const kStrippedSchema = Symbol("pi.schema.descriptions.stripped");
|
||||
|
||||
function postProcessJsonSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
||||
walk(schema);
|
||||
// xAI 400s only when the *tool root* is object + typeless exclusive-required
|
||||
// anyOf. Nested unions (task.outputSchema, etc.) must stay intact.
|
||||
flattenExclusiveRequiredUnion(schema);
|
||||
normalizeArkPropertyComments(schema);
|
||||
normalizeEmptySchemas(schema);
|
||||
return schema;
|
||||
@@ -440,7 +443,6 @@ function walk(node: unknown): void {
|
||||
if (!node || typeof node !== "object") return;
|
||||
const obj = node as Record<string, unknown>;
|
||||
rewriteNullableScalarAnyOf(obj);
|
||||
flattenExclusiveRequiredUnion(obj);
|
||||
inferBareEnumScalarType(obj);
|
||||
collapseConstUnionAnyOf(obj);
|
||||
for (const k in obj) walk(obj[k]);
|
||||
|
||||
@@ -630,6 +630,28 @@ describe("sanitizeSchemaForOpenAIResponses", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not flatten nested exclusive-required anyOf (xAI only rejects the tool root)", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
paths: { type: "array", items: { type: "string" } },
|
||||
scopes: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
anyOf: [{ required: ["paths"] }, { required: ["scopes"] }],
|
||||
},
|
||||
},
|
||||
required: ["outputSchema"],
|
||||
};
|
||||
const sanitized = sanitizeSchemaForOpenAIResponses(schema);
|
||||
expect(sanitized.anyOf).toBeUndefined();
|
||||
const outputSchema = (sanitized.properties as Record<string, unknown>).outputSchema as Record<string, unknown>;
|
||||
expect(outputSchema.anyOf).toEqual([{ required: ["paths"] }, { required: ["scopes"] }]);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -105,6 +105,29 @@ describe("toolWireSchema — raw JSON Schema normalization", () => {
|
||||
expect(wire.required).toEqual(["project"]);
|
||||
});
|
||||
|
||||
it("does not flatten nested exclusive-required anyOf (only the tool root 400s xAI)", () => {
|
||||
const wire = toolWireSchema(
|
||||
jsonTool({
|
||||
type: "object",
|
||||
properties: {
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
paths: { type: "array", items: { type: "string" } },
|
||||
scopes: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
anyOf: [{ required: ["paths"] }, { required: ["scopes"] }],
|
||||
},
|
||||
},
|
||||
required: ["outputSchema"],
|
||||
}),
|
||||
);
|
||||
expect(wire.anyOf).toBeUndefined();
|
||||
const outputSchema = (wire.properties as Record<string, unknown>).outputSchema as Record<string, unknown>;
|
||||
expect(outputSchema.anyOf).toEqual([{ required: ["paths"] }, { required: ["scopes"] }]);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it("preserves raw JSON Schema required defaults and safe-integer bounds", () => {
|
||||
const wire = toolWireSchema(
|
||||
|
||||
Reference in New Issue
Block a user