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
// 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 его нет.
Стриминг
Для потоковых ответов передайте "stream": true и включите в reqwest фичу stream. Ответ приходит как Server-Sent Events: строки вида data: {json} с фрагментом в choices[0].delta.content, а в конце — data: [DONE]. Читайте байты через bytes_stream(), собирайте строки до перевода строки и пропускайте чанки с пустым choices — в последнем чанке Heli передаёт usage.
async-openai
Клиент также читает адрес из переменной OPENAI_BASE_URL. Модель указывайте полным слагом каталога Heli. Типы запросов в async-openai менялись между версиями, поэтому сверяйте примеры с документацией своей версии крейта.
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);Типы и параллельность
Вместо 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 и Яндекс — сразу в кабинете.