Перейти к основному содержимому

Строгий разбор и лимиты

Разбор ответа строгий по умолчанию: всё, чего 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-8ProtocolПропускаются или заменяются с потерями
Меняющиеся id или model, расход токенов дважды или неполныйProtocolДопускается
Больше одного варианта ответаUnsupportedЧитается первый
Неизвестная причина завершения или контент после причины завершенияUnsupportedUnsupported

Оба режима соблюдают лимиты, проваливают поток, закончившийся раньше времени, и отвергают несогласованные вызовы инструментов: отсутствующие или повторяющиеся 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 eventevent_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()
}