Heli + Rust: reqwest и async-openai

Heli — OpenAI-совместимый REST API, поэтому в Rust не нужен специальный SDK. Достаточно reqwest и serde_json: POST на https://getheli.ru/v1/chat/completions с заголовком Authorization: Bearer sk-heli-…. Если удобнее типизированный клиент, подойдёт крейт async-openai с другим base URL.

reqwest + serde_json

code
// Cargo.toml:
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("HELI_API_KEY")?;
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(90))
        .build()?;

    let body = json!({
        "model": "deepseek/deepseek-v4-flash",
        "messages": [{"role": "user", "content": "Объясни ownership в Rust в двух предложениях."}],
        "max_tokens": 300
    });
    let resp = client
        .post("https://getheli.ru/v1/chat/completions")
        .bearer_auth(api_key)
        .json(&body)
        .send()
        .await?;

    let status = resp.status();
    let data: Value = resp.json().await?;
    if !status.is_success() {
        eprintln!("Heli {}: {}", status, data["error"]["message"]);
        return Ok(());
    }
    println!("{}", data["choices"][0]["message"]["content"].as_str().unwrap_or(""));
    Ok(())
}

Ошибки и повторы

  • Ошибки приходят в формате OpenAI: error.message, error.type и error.code. Логируйте статус и message, но не тело запроса с ключом.
  • Повторять стоит 429 и 5xx с паузой. Для 429 читайте заголовок Retry-After, для 5xx используйте экспоненциальный backoff и не больше трёх попыток.
  • 401 и 402 не повторяйте: нужно исправить ключ или пополнить баланс.
  • Таймаут клиента задавайте явно — по умолчанию у reqwest его нет.

Ошибка 429

Ошибки 500, 502 и 504

Стриминг

Для потоковых ответов передайте "stream": true и включите в reqwest фичу stream. Ответ приходит как Server-Sent Events: строки вида data: {json} с фрагментом в choices[0].delta.content, а в конце — data: [DONE]. Читайте байты через bytes_stream(), собирайте строки до перевода строки и пропускайте чанки с пустым choices — в последнем чанке Heli передаёт usage.

Streaming в Heli API

async-openai

Клиент также читает адрес из переменной OPENAI_BASE_URL. Модель указывайте полным слагом каталога Heli. Типы запросов в async-openai менялись между версиями, поэтому сверяйте примеры с документацией своей версии крейта.

code
use async_openai::{config::OpenAIConfig, Client};

let config = OpenAIConfig::new()
    .with_api_base("https://getheli.ru/v1")
    .with_api_key(std::env::var("HELI_API_KEY")?);
let client = Client::with_config(config);

DeepSeek V4 Flash

GPT-5.4 mini

Переменные окружения для Heli

Документация API

Типы и параллельность

Вместо serde_json::Value можно описать ответ структурами с derive(Deserialize): choices с message.content и finish_reason, usage с prompt_tokens и completion_tokens. По умолчанию serde пропускает неизвестные поля, поэтому новые поля в ответе API не сломают разбор.

Создайте один reqwest::Client на всё приложение и переиспользуйте его: внутри он держит пул соединений, а клонирование дешёвое. Для пакетной обработки ограничивайте число одновременных запросов, например через tokio::sync::Semaphore или buffer_unordered из крейта futures. Тысяча запросов разом упрётся в лимиты и получит 429.

Попробуйте Heli

Ключ sk-heli- появляется после подтверждения почты. Через Google и Яндекс — сразу в кабинете.

Документация API · Модели и цены

Читайте также