Строгий разбор и лимиты
Разбор ответа строгий по умолчанию: всё, чего svir не знает или что не сходится, — ошибка, а не догадка. Мягкий режим ослабляет только то, что не может сделать ответ неверным.
use svir::prelude::*;
fn lenient() -> Result<Client, Error> {
// For a server with extensions you do not need.
Client::openai("http://127.0.0.1:1234").lenient().build()
}
.lenient() — сокращение для .mode(svir::Mode::Lenient). У отдельного
декодера тот же выбор: Decoder::strict() или
Decoder::lenient().
Строгий и мягкий
| Сервер присылает | Строгий | Мягкий |
|---|---|---|
| Неизвестное svir поле в дельте или вызове инструмента | Unsupported | Пропускается |
| Тип события или поле SSE вне протокола | Unsupported | Пропускается |
| Данные, которые не являются JSON, или невалидный UTF-8 | Protocol | Пропускаются или заменяются с потерями |
Меняющиеся id или model, расход токенов дважды или неполный | Protocol | Допускается |
| Больше одного варианта ответа | Unsupported | Читается первый |
| Неизвестная причина завершения или контент после причины завершения | Unsupported | Unsupported |
Оба режима соблюдают лимиты, проваливают поток, закончившийся раньше времени, и отвергают несогласованные вызовы инструментов: отсутствующие или повторяющиеся ID, пропуск в индексах или причину завершения, которая не согласуется с вызовами.
Отказ, присланный в refusal вместо content, читается в обоих режимах: это
текст ответа, а причина завершения — Refusal. Ответ, который одновременно и
содержимое, и отказ, или отказ с вызовами инструментов — это Protocol в
обоих.
Контент-фильтр Azure OpenAI присылает чанки, которые ничего не несут из ответа, и оба режима читают их одинаково:
- Отчёт по промпту (чанк без вариантов и с
prompt_filter_results, приходит первым) пропускается. - Аннотация асинхронного контент-фильтра (вариант ответа с
content_filter_offsetsи без дельты) — вердикт по уже отправленному тексту. Аннотация, которая ничего не блокирует, пропускается — до причины завершения или после неё. Блокировка — причина завершенияcontent_filterили вердикт сfiltered: true— делает причиной завершения ответаContentFilter, даже после собственногоstopмодели. Любая другая причина завершения в аннотации —Unsupported.
Что выбрать
- Строгий — для сервера под вашим контролем и чтобы узнать, что на самом деле присылает новый сервер.
- Мягкий — для сервера с расширениями, которые вам не нужны, и для прокси, где судить о незнакомых svir полях будет клиент ниже по цепочке.
Мягкий режим — не лекарство от ошибки, которую вы не прочитали. Чтобы увидеть,
что присылает сервер, запустите против него пример relay из репозитория
svir: он печатает сырой поток.
Лимиты
У каждого ответа есть рамки — в обоих режимах. Упереться в них —
ErrorKind::ResponseLimit, а не зависание или неограниченная аллокация.
| Лимит | Метод | По умолчанию |
|---|---|---|
| Байты всего ответа по мере прихода | wire_bytes(n) | 64 МиБ |
| Байты одного server-sent event | event_bytes(n) | 256 КиБ |
| Вызовы инструментов в одном ответе | tool_calls(n) | 64 |
use svir::Limits;
use svir::prelude::*;
fn tuned() -> Result<Client, Error> {
let limits = Limits::default()
.event_bytes(1024 * 1024)
.tool_calls(16);
Client::openai("http://127.0.0.1:1234").limits(limits).build()
}
Лимит на байты считает поток событий, который тратит несколько сотен байт на каждый токен: 64 МиБ — это примерно четверть миллиона токенов. Декодер хранит ответ, а не байты с провода, так что лимит защищает от сервера, который никогда не заканчивает, а не от расхода памяти.
Inline-теги <think>
Некоторые серверы оставляют рассуждения модели внутри текста ответа, между
<think> и </think>. По умолчанию svir вырезает их в Event::Reasoning с
источником Think — даже если тег разрезан границей чанка. Чтобы сохранить
текст ровно как прислан, вместе с тегами:
use svir::Think;
use svir::prelude::*;
fn keep_tags() -> Result<Client, Error> {
Client::openai("http://127.0.0.1:1234").think(Think::Keep).build()
}