source dump of claude code
at main 36 lines 1.5 kB view raw
1import { z } from 'zod/v4' 2 3/** 4 * Number that also accepts numeric string literals like "30", "-5", "3.14". 5 * 6 * Tool inputs arrive as model-generated JSON. The model occasionally quotes 7 * numbers — `"head_limit":"30"` instead of `"head_limit":30` — and z.number() 8 * rejects that with a type error. z.coerce.number() is the wrong fix: it 9 * accepts values like "" or null by converting them via JS Number(), masking 10 * bugs rather than surfacing them. 11 * 12 * Only strings that are valid decimal number literals (matching /^-?\d+(\.\d+)?$/) 13 * are coerced. Anything else passes through and is rejected by the inner schema. 14 * 15 * z.preprocess emits {"type":"number"} to the API schema, so the model is 16 * still told this is a number — the string tolerance is invisible client-side 17 * coercion, not an advertised input shape. 18 * 19 * .optional()/.default() go INSIDE (on the inner schema), not chained after: 20 * chaining them onto ZodPipe widens z.output<> to unknown in Zod v4. 21 * 22 * semanticNumber() → number 23 * semanticNumber(z.number().optional()) → number | undefined 24 * semanticNumber(z.number().default(0)) → number 25 */ 26export function semanticNumber<T extends z.ZodType>( 27 inner: T = z.number() as unknown as T, 28) { 29 return z.preprocess((v: unknown) => { 30 if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v)) { 31 const n = Number(v) 32 if (Number.isFinite(n)) return n 33 } 34 return v 35 }, inner) 36}