Содержание
Коротко
Передаёте схему Zod 4 в помощник структурированного вывода — и каждое .refine() молча выпадает, прежде чем модель увидит правила.
OpenAI Node SDK и Vercel AI SDK вызывают z.toJSONSchema(). В карте contributors у конвертера Zod 4.6.7 всего 13 ключей; .refine() лежит как проверка custom и в эту карту не входит.
Сама доработка всё ещё срабатывает при разборе ответа. Модель просто никогда не была ею ограничена.
Что произошло
Автор исходной заметки разбирает разрыв между схемой, которую вы написали, и схемой, которую реально видит модель.
Представьте багаж на контроле: в чемодане и паспорт, и рукописная записка «не класть ссылки в поле note». Сканер пропускает только то, что есть в списке из тринадцати категорий. Записку снимают без объявления. Вы узнаёте об этом уже в зале выдачи — когда чемодан открыли и сверили с вашим списком.
В json-schema-processors.ts одна строка решает, какие проверки попадут в JSON Schema: для каждой проверки ищут обработчик в contributors. Там есть greater_than, less_than, multiple_of, min_length, string_format и соседние ключи. А .refine() в api.ts сохраняется как custom — и необязательный вызов ничего не находит. Правило пропускают.
Автор собирает схему извлечения возврата из писем поддержки. Четыре поля выражаются в JSON Schema напрямую: .regex(), .positive(), z.enum, z.iso.date(). Остальное — проблема: .refine() на поле note (без ссылок), .refine() на всём объекте (дата возврата не раньше заказа) и .transform() на sku.
В режиме { io: "input" }, которым пользуется Vercel AI SDK, модель видит pattern, exclusiveMinimum, enum, format: "date". Поле note становится голой строкой. Объектное уточнение исчезает. sku в режиме input тоже просто строка; в режиме output конвертер может бросить ошибку — Transforms cannot be represented in JSON Schema.
Тесты Zod подтверждают картину: два z.string().refine(...) в объединении схлопываются в "type": "string". То же ждёт .superRefine() и объектный .refine() — всё это снова custom.
Отдельно — провайдер. Даже попавшее в JSON Schema ключевое слово модель может не держать жёстко. У Anthropic числовые ограничения вроде minimum / multipleOf и длины строк в SDK часто уходят в текст описания поля, а не в декодирующее ограничение. .positive() даёт exclusiveMinimum — тот же класс. У OpenAI шире набор (pattern, format, границы, anyOf), но allOf, not, if/then/else не поддержаны; помощник ещё переписывает oneOf в anyOf.
Почему это важно
На демо схема «зелёная»: типы сходятся, помощник не ругается. В продукте модель спокойно кладёт URL в note или меняет даты местами — потому что в JSON Schema этих правил не было.
Ошибка всплывает только на parse. Если вы бросаете исключение и не отдаёте текст ошибки обратно, пользователь видит сбой, а не исправленный JSON.
Разрыв «я написал схему» ↔ «модель увидела схему» — одна из первых ловушек при переходе от прототипа к функции, на которую опираются люди. Доработки и преобразования живут в Zod; структурированный вывод живёт в подмножестве JSON Schema плюс то, что провайдер ещё не вырезал.
На практике
Автор предлагает две линии защиты и вынести преобразования из схемы, которую отправляют по сети.
- Каждое правило, которое модель должна соблюдать, продублируйте в
.describe()— метаданныеZodкопируются вJSON Schema. Ставьте.describe()последним в цепочке: после.refine()метаданные «отваливаются», потому что каждый метод возвращает новый экземпляр. - Оставьте
.refine()как вторую линию: они всё ещё бегут при разборе ответа. - Уберите
.transform()из схемы, которую отдаёте в помощник. На проводе — простые типы; обрезку иtoUpperCaseдляskuсделайте уже после успешного разбора. - Не полагайтесь только на
responses.parse, который бросает при ошибке. Вызовитеresponses.create, разберите текст черезsafeParseи при провале один раз верните моделиz.prettifyError(...)с просьбой прислать исправленный JSON. - В объектных
.refine()задавайтеpath: тогда в сообщении для модели появится имя поля. - Остановитесь на одной повторной попытке. Два провала с текстом ошибки перед глазами — уже повод логировать сырой вывод и решать, правило в инструкции или в коде.
- Перед
JSON.parseпроверьте статус ответа: обрыв или отказ модели иначе маскируются под ошибку разбора.
Итог
.refine() в Zod 4 не едет в JSON Schema: в карте конвертера нет ключа custom. Модель видит .regex(), .positive(), enum и даты ISO; полевые и объектные доработки — нет. Преобразования либо врут тип в режиме input, либо падают в output.
Починка простая по форме: описать правило словами через .describe(), оставить доработки второй линией, вынести преобразования, проверить safeParse и один раз скормить модели z.prettifyError.
Багаж прошёл контроль. Сверьте список сами — сканер не объявлял, что снял.



Комментарии