Кодек сам по себе
Encoder и Decoder — то, из чего собран клиент, и они публичные. С
default-features = false svir — это только типы и кодек: ни клиента, ни
hyper, ни Tokio.
[dependencies]
svir = { version = "0.1.5", default-features = false }
Если единственная причина — другой HTTP-клиент, то собственный бэкенд проще: он сохраняет разбор статусов, таймауты, обработку совместимости и слои.
Декодер
Decoder работает по push-модели и не делает I/O: байты на входе, события на
выходе. Он читает и поток, которым владеет, и поток, который параллельно
передаётся дальше.
use svir::openai::chat::Decoder;
use svir::prelude::*;
const RESPONSE: &str = concat!(
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"finish_reason\":null}]}\n\n",
"data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n",
"data: [DONE]\n\n",
);
fn decode() -> Result<Option<Completion>, Error> {
let mut decoder = Decoder::strict();
let mut answer = None;
// The pieces can be cut anywhere: inside a line, a string, a character.
for piece in RESPONSE.as_bytes().chunks(7) {
for event in decoder.push(piece) {
if let Event::Completed(done) = event? {
answer = Some(done);
}
}
}
decoder.finish()?;
Ok(answer)
}
| Вызов | Что делает |
|---|---|
Decoder::strict() / Decoder::lenient() / Decoder::new(mode) | Декодер для одного ответа |
.limits(limits) / .think(think) | Задаются до первого push |
push(&bytes) | Возвращает то, что эти байты завершили, как Vec<Result<Event, Error>> в порядке провода |
is_done() | Завершился ли поток — успешно или с ошибкой |
finish() | Завершает ввод; поток, который так и не завершился, — TruncatedStream |
Правила, которые держит декодер и на которые может полагаться код вокруг:
- Не больше одного элемента
push— ошибка, и это последний элемент. ПослеEvent::Completedили ошибкиpushничего не возвращает. - Результат не зависит от того, как нарезаны байты. Один и тот же ответ, прочитанный побайтно или целиком, даёт одни и те же события.
- Один декодер читает один ответ. Для следующего создайте новый.
finishсообщает об обрыве один раз; после ошибки, уже возвращённой изpush, он возвращаетOk.Completion::timingотсчитывается от создания декодера, поэтому создавайте его, когда ответ начался.
Кодировщик
Encoder превращает Request в Body, точная длина которого известна до
первого байта.
use svir::openai::chat::Encoder;
use svir::prelude::*;
fn encode(request: &Request) -> Result<(u64, bytes::Bytes), Error> {
let body = Encoder::new().include_usage(true).encode(request)?;
let length = body.len();
// `into_bytes` works when no attachment is a file path.
Ok((length, body.into_bytes()?))
}
| Вызов | Что делает |
|---|---|
Encoder::new() | Отправляет то, что задано в запросе, и ничего больше |
.include_usage(bool) | Запрашивает расход токенов, если запрос не говорит иного. Здесь выключено; клиент его включает |
.lean(bool) | Опускает reasoning_effort и stream_options — для сервера, который, как известно, их отвергает. Выбор инструмента и формат ответа остаются |
.context_tokens(n) | Падает с ContextOverflow, если длина тела плюс max_tokens не помещается |
encode(&request) | Без I/O. Вложение, заданное путём к файлу, — ErrorKind::Attachment; обязательный вызов инструмента, который запрос сделать не может, — Unsupported |
encode_files(&request).await | Фича client. Сначала измеряет файлы-вложения, чтобы длина была точной; текстовый файл с объявленным escaped_len не читается |
body.len() | Content-Length для отправки |
body.into_bytes() | Всё тело целиком, когда все вложения в памяти |
body.into_stream() | Фича client. Тело блоками; файлы читаются по мере опроса |
Тело всегда говорит "stream": true: у svir нет декодера для ответа, который
не является потоком.
Собственный транспорт
Без клиента транспорт должен делать то, что делает клиент:
POST {base}/v1/chat/completionsсcontent-type: application/json,accept: text/event-stream, заголовкомAuthorization, если есть ключ, иContent-Lengthизbody.len().- Считать любой неуспешный статус ошибкой ещё до разбора. Соответствие
статусов и
ErrorKindживёт в клиенте и не входит в кодек; с собственным транспортом это соответствие — ваше. - Требовать
content-type: text/event-streamпри успехе. Всё остальное — не поток ответа. - Передавать тело в
Decoderпо мере прихода и вызватьfinishв конце. - Прекратить чтение, когда пришёл
Event::Completed.