fix(ai): preserved multi-type ollama tool unions

- Kept nullable single-type arrays flattened for Ollama while expanding true multi-type arrays into anyOf alternatives.

- Covered non-null string/number and object/array unions in the Ollama provider payload regression test.

Refs #4488
This commit is contained in:
roboomp
2026-07-04 04:41:51 +00:00
parent df45f85885
commit d4545bc59f
2 changed files with 20 additions and 3 deletions
+13 -2
View File
@@ -1086,6 +1086,7 @@ export function sanitizeSchemaForOllama(schema: JsonObject): JsonObject {
let changed = false;
const output: JsonObject = {};
let typeAlternatives: JsonObject[] | undefined;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
const child = value[key];
@@ -1095,8 +1096,13 @@ export function sanitizeSchemaForOllama(schema: JsonObject): JsonObject {
}
if (key === "type" && Array.isArray(child)) {
const variants = child.filter((entry): entry is string => typeof entry === "string");
const nonNull = variants.filter(entry => entry !== "null");
output.type = nonNull[0] ?? variants[0] ?? child[0];
const uniqueVariants = [...new Set(variants)];
const nonNull = uniqueVariants.filter(entry => entry !== "null");
if (nonNull.length <= 1) {
output.type = nonNull[0] ?? uniqueVariants[0] ?? child[0];
} else {
typeAlternatives = uniqueVariants.map(entry => ({ type: entry }));
}
changed = true;
continue;
}
@@ -1128,6 +1134,11 @@ export function sanitizeSchemaForOllama(schema: JsonObject): JsonObject {
output[key] = next;
}
if (typeAlternatives) {
const existingAnyOf = output.anyOf;
output.anyOf = Array.isArray(existingAnyOf) ? [...typeAlternatives, ...existingAnyOf] : typeAlternatives;
}
return changed ? output : value;
};
return normalizeNode(schema) as JsonObject;
@@ -101,6 +101,8 @@ describe("Ollama chat thinking controls", () => {
properties: {
anything: {},
nullableName: { type: ["string", "null"] },
stringOrNumber: { type: ["string", "number"] },
objectOrArray: { type: ["object", "array"] },
list: { type: "array", items: {} },
union: { anyOf: [{}, { type: "string" }] },
nested: {
@@ -109,7 +111,7 @@ describe("Ollama chat thinking controls", () => {
additionalProperties: false,
},
},
required: ["anything", "nullableName", "list", "union", "nested"],
required: ["anything", "nullableName", "stringOrNumber", "objectOrArray", "list", "union", "nested"],
additionalProperties: false,
},
};
@@ -143,6 +145,10 @@ describe("Ollama chat thinking controls", () => {
expect(Object.hasOwn(parameters, "additionalProperties")).toBe(false);
expect(properties.anything).toEqual(widenedOpen);
expect(properties.nullableName?.type).toBe("string");
expect(properties.stringOrNumber?.anyOf).toEqual([{ type: "string" }, { type: "number" }]);
expect(Object.hasOwn(properties.stringOrNumber, "type")).toBe(false);
expect(properties.objectOrArray?.anyOf).toEqual([{ type: "object" }, { type: "array" }]);
expect(Object.hasOwn(properties.objectOrArray, "type")).toBe(false);
expect(properties.list?.items).toEqual(widenedOpen);
expect(properties.union?.anyOf).toEqual([widenedOpen, { type: "string" }]);
expect(Object.hasOwn(properties.nested, "additionalProperties")).toBe(false);