fix: normalize string booleans in localSearch schema for Qwen 3.5 (#2232) (#2243)

Qwen 3.5 (LM Studio) emits returnAll as string "True"/"False" instead
of boolean, causing Zod schema validation to fail. Add z.preprocess()
to coerce string booleans to actual booleans before validation.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Logan Yang 2026-03-02 20:38:51 -08:00 committed by GitHub
parent f67e835afb
commit 2bab7acb05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 2 deletions

View file

@ -19,6 +19,14 @@ describe("SearchTools Schema Validation", () => {
})
.optional()
.describe("Time range for search"),
returnAll: z
.preprocess((val) => {
if (typeof val === "string") {
return val.toLowerCase() === "true";
}
return val;
}, z.boolean().optional())
.describe("Return all matching notes"),
});
test("validates correct input structure", () => {
@ -100,6 +108,69 @@ describe("SearchTools Schema Validation", () => {
const result = localSearchSchema.safeParse(invalidInput);
expect(result.success).toBe(false);
});
test("accepts boolean returnAll: true", () => {
const input = { query: "find all notes", salientTerms: ["notes"], returnAll: true };
const result = localSearchSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.returnAll).toBe(true);
}
});
test("accepts boolean returnAll: false", () => {
const input = { query: "find notes", salientTerms: ["notes"], returnAll: false };
const result = localSearchSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.returnAll).toBe(false);
}
});
test("coerces string 'True' to boolean true", () => {
const input = { query: "find all notes", salientTerms: ["notes"], returnAll: "True" };
const result = localSearchSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.returnAll).toBe(true);
}
});
test("coerces string 'true' to boolean true", () => {
const input = { query: "find all notes", salientTerms: ["notes"], returnAll: "true" };
const result = localSearchSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.returnAll).toBe(true);
}
});
test("coerces string 'FALSE' to boolean false", () => {
const input = { query: "find notes", salientTerms: ["notes"], returnAll: "FALSE" };
const result = localSearchSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.returnAll).toBe(false);
}
});
test("coerces string 'False' to boolean false", () => {
const input = { query: "find notes", salientTerms: ["notes"], returnAll: "False" };
const result = localSearchSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.returnAll).toBe(false);
}
});
test("accepts omitted returnAll (optional)", () => {
const input = { query: "find notes", salientTerms: ["notes"] };
const result = localSearchSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.returnAll).toBeUndefined();
}
});
});
describe("webSearchTool schema", () => {

View file

@ -77,8 +77,12 @@ const localSearchSchema = z.object({
.optional()
.describe("Optional time range filter. Use epoch milliseconds from getTimeRangeMs result."),
returnAll: z
.boolean()
.optional()
.preprocess((val) => {
if (typeof val === "string") {
return val.toLowerCase() === "true";
}
return val;
}, z.boolean().optional())
.describe(
"Set to true when the user wants ALL matching notes, not just the best few. " +
"Use for requests like 'find all my X', 'list every Y', 'show me all my Z', " +