diff --git a/packages/ai/src/utils/schema/normalize.ts b/packages/ai/src/utils/schema/normalize.ts index 0b1bbd756..d17e439bf 100644 --- a/packages/ai/src/utils/schema/normalize.ts +++ b/packages/ai/src/utils/schema/normalize.ts @@ -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; diff --git a/packages/ai/test/ollama-thinking-disable.test.ts b/packages/ai/test/ollama-thinking-disable.test.ts index 6116414b5..080044462 100644 --- a/packages/ai/test/ollama-thinking-disable.test.ts +++ b/packages/ai/test/ollama-thinking-disable.test.ts @@ -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);