Настройка
Client::openai(url) возвращает ClientBuilder. Каждый метод принимает
self и возвращает его, а build() возвращает Result<Client, Error>: URL,
источник ключа и заголовки проверяются здесь, а не при первом запросе.
use std::time::Duration;
use svir::layer::{Retry, Timeout};
use svir::prelude::*;
fn client() -> Result<Client, Error> {
Client::openai("https://models.example.com/v1")
.api_key_env("MODELS_API_KEY")
.connect_timeout(Duration::from_secs(5))
.layer(Retry::connect(3))
.layer(Timeout::first_token(Duration::from_secs(120)))
.build()
}
| Метод | Что делает | По умолчанию |
|---|---|---|
api_key(key) / api_key_env(name) / api_key_file(path) | Bearer-аутентификация | Нет |
header(name, value) | Заголовок, который уходит с каждым запросом, — для шлюза или хостингового эндпоинта | Нет |
allow_http() | Обычный HTTP к хосту, который не loopback | Запрещено |
connect_timeout(d) | Сколько может длиться подключение | 10 с |
idle_timeout(d) / no_idle_timeout() | Сколько сервер может ничего не присылать | 5 мин |
include_usage(bool) | Запрашивать расход токенов, если запрос не говорит иного | Включено |
lenient() / mode(Mode) | Насколько строго читаются ответы | Строго |
limits(Limits) | Лимиты на один ответ | 64 МиБ, 256 КиБ на событие, 64 вызова инструментов |
think(Think) | Что делать с inline-тегами <think> | Вырезать в рассуждения |
context_tokens(n) | Отклонять запрос, который не поместится, ещё до отправки | Выключено |
http(backend) | Другой HTTP-бэкенд | hyper |
layer(l) / wrap(closure) | Middleware вокруг каждого вызова | Нет |
Создайте один Client на сервер и клонируйте его. Клоны дешёвые и делят
пул соединений и то, что клиент узнал о сервере. Debug клиента показывает
URL и никогда — ключ или значение заголовка.
Строгости и лимитам посвящена отдельная страница: Строгий разбор и лимиты.
Базовый URL
Передайте базу сервера — с /v1 и завершающим слешем или без; svir сам
добавит /v1/chat/completions и /v1/models. Путь перед этим сохраняется:
http://host/openai/v1 вызывает http://host/openai/v1/chat/completions.
build() завершается с ErrorKind::Config, если:
| URL | Почему |
|---|---|
http:// к хосту, который не loopback | Ключи и промпты пошли бы по сети открытым текстом. Используйте https:// или .allow_http() для доверенной сети |
https:// без фичи tls или tls-aws-lc | Ни одна не включена; см. Фичи и TLS |
| Учётные данные, query или fragment в URL | Ключу место в api_key, а не в URL |
Нет схемы (127.0.0.1:1234) | Пишите http://127.0.0.1:1234 |
Loopback — это localhost, 127.0.0.1 и [::1].
API-ключи
| Источник | Что читается |
|---|---|
.api_key("...") | Переданное значение |
.api_key_env("NAME") | Переменная, при сборке клиента; если её нет — ошибка Config |
.api_key_file(path) | Файл, при сборке клиента; пробелы по краям отбрасываются |
svir не читает ни переменных окружения, ни файлов .env, пока его об этом
не попросят. Ключ отправляется как Authorization: Bearer ... и никогда не
появляется в Debug, Display, ошибках или событиях. Локальному серверу без
аутентификации ключ не нужен вовсе.
Дополнительные заголовки
Шлюз или хостинговый эндпоинт может требовать собственный заголовок: для
атрибуции, организации или проекта либо ключ под другим именем, не
Authorization. header(name, value) добавляет его к каждому запросу
клиента, включая список моделей.
use svir::prelude::*;
fn client() -> Result<Client, Error> {
Client::openai("https://gateway.example.com/v1")
.api_key_env("GATEWAY_KEY")
.header("x-title", "My App")
.build()
}
- Имена не зависят от регистра и отправляются в нижнем регистре. Повторная установка того же имени заменяет прежнее значение.
- Любое значение считается учётными данными: оно не появляется в
Debug, ошибках и событиях, а встроенный бэкенд отправляет его с пометкой sensitive. Ошибка называет заголовок, но не его значение. - svir не читает переменные окружения для заголовков. Если значение лежит
там, прочитайте его через
std::env::var(..)и передайте.
build() завершается с ErrorKind::Config, если:
| Заголовок | Почему |
|---|---|
Имя, которое не может быть именем заголовка ("x title", "") | Его нельзя отправить |
| Значение с переводом строки, другим управляющим символом или не-ASCII текстом | Перевод строки закончил бы заголовок и начал другой |
authorization, content-type, content-length, accept, host, transfer-encoding, connection | Их svir пишет сам, или они задают рамки запроса. Bearer-ключ передаётся через api_key |
Заголовки принадлежат клиенту и одинаковы для всех запросов. Заголовков на отдельный запрос нет: слой работает над HTTP и добавить заголовок не может.
Таймауты
| Что | Как | По умолчанию |
|---|---|---|
| Подключение | connect_timeout(d) | 10 с |
| Тишина со стороны сервера — до ответа и во время него | idle_timeout(d) | 5 мин |
| Время до первого куска ответа | слой Timeout::first_token(d) | Нет |
| Весь ответ | слой Timeout::total(d) | Нет |
Общего таймаута по умолчанию нет — намеренно: долгая генерация не ошибка. Таймаут простоя длинный, потому что локальная модель читает весь промпт до первого токена. Сокращайте его для облачного сервера, а не для локального.
Все они завершаются с ErrorKind::Timeout.
Список моделей
use svir::prelude::*;
async fn models(client: &Client) -> Result<Vec<String>, Error> {
let listed = client.list_models().await?;
Ok(listed.into_iter().map(|model| model.id).collect())
}
Список — ровно то, что сообщает сервер, без фильтрации: модели эмбеддингов и
речи стоят в нём рядом с чат-моделями. У Model есть id — ID для запроса —
и необязательное отображаемое имя name.
Некоторые локальные серверы отвечают той моделью, что сейчас загружена, если не знают ID из запроса. Если любая модель отвечает одинаково — выведите список моделей и проверьте ID.
Обработка совместимости
Некоторые серверы отвергают необязательные поля reasoning_effort и
stream_options с кодом 400 или 422. Тогда клиент отправляет запрос ещё
раз без них и запоминает это: следующие запросы этого клиента и его клонов
сразу обходятся без них. Отвергнутый запрос ничего не сгенерировал, поэтому
это не повтор в смысле слоя Retry, и настраивать тут ничего не нужно.
Что важно знать:
- На таком сервере
Completion::usageравенNone(расход токенов запрашивается черезstream_options), а глубина рассуждений не применяется. - Вторую попытку получает только отказ, тело которого не объясняет причину. Переполнение контекста или промпт, заблокированный контент-фильтром, сообщаются как есть: отправленный снова, заблокированный промпт был бы оплачен ещё раз.
- Если вторая попытка тоже падает, сообщается исходная ошибка.
- Выбор инструмента и
формат ответа не являются необязательными:
ответ должен им соответствовать. Вторая попытка их сохраняет, запрос с ними
и без необязательных полей дважды не отправляется, а сервер, который их не
принимает, проваливает запрос с
Unsupportedи собственными словами.