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

Настройка

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 и собственными словами.