Rust и async-openai: подключаем Claude через OpenAI-совместимый API
Как подключить Claude к Rust-приложению: async-openai с with_api_base, флаг chat-completion, стриминг, ограничение параллельности и обработка ошибок. Вариант на reqwest для минимума зависимостей.
4 мин · · разработка
Для Rust нет нужды ждать отдельного SDK под каждый API: Zukko принимает запросы в OpenAI-формате по адресу https://api.zukko.pro/v1, поэтому Claude подключается через крейт async-openai или напрямую через reqwest. Ниже — оба варианта на актуальной версии async-openai 0.42, стриминг с правильной обработкой последнего чанка, параллельные запросы с ограничением и разбор ошибок.
Зависимости
[package]
name = "zukko-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
async-openai = { version = "0.42", features = ["chat-completion"] }
futures = "0.3"
reqwest = { version = "0.13", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }Главная ловушка новых версий async-openai: API разбиты на feature-флаги, и по умолчанию включён только TLS. Без features = ["chat-completion"] у клиента не будет метода chat(), и код из старых примеров не скомпилируется. Типы чата теперь лежат в модуле async_openai::types::chat. Список флагов — на странице крейта на docs.rs.
reqwest и serde нужны только для первого варианта; если используете лишь async-openai, их можно убрать.
Ключ fetch_... создайте в кабинете Zukko и положите в переменную окружения: export ZUKKO_API_KEY=fetch_....
Вариант 1: reqwest и serde
Минимум абстракций: обычный POST и разбор JSON в свои структуры.
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct ChatResponse {
choices: Vec<Choice>,
usage: Option<Usage>,
}
#[derive(Deserialize)]
struct Choice {
message: Message,
}
content объявлен как Option<String>: в OpenAI-формате поле может прийти null (например, если модель вернула вызов инструмента), и String без Option уронил бы десериализацию.
Вариант 2: async-openai с Zukko
Адрес API переопределяется методом with_api_base у OpenAIConfig, ключ — with_api_key. Запрос собирается билдером CreateChatCompletionRequestArgs, как в официальных примерах крейта.
use async_openai::{
config::OpenAIConfig,
types::chat::{
ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage,
CreateChatCompletionRequestArgs,
},
Client,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = OpenAIConfig::new()
.with_api_base("https://api.zukko.pro/v1")
.with_api_key(std::env::var("ZUKKO_API_KEY")?);Ответ модели лежит в choice.message.content типа Option<String>. В старых версиях статей встречается разбор ответа через ChatCompletionRequestMessage::Assistant — это тип запроса, а не ответа, такой код не компилируется.
Стриминг ответа
create_stream возвращает поток Result<CreateChatCompletionStreamResponse, OpenAIError>. Последний чанк от Zukko содержит только usage, а массив choices в нём пустой. Поэтому берём choices.first() через if let, а не индексом [0], который на этом чанке вызвал бы панику.
use std::io::Write;
use async_openai::{
config::OpenAIConfig,
types::chat::{ChatCompletionRequestUserMessage, CreateChatCompletionRequestArgs},
Client,
};
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = OpenAIConfig::new()
.with_api_base("https://api.zukko.pro/v1")
.with_api_key(std::env::var("ZUKKO_API_KEY")?);flush() нужен, потому что stdout буферизуется построчно, и без него текст появлялся бы рывками.
Параллельные запросы с ограничением
Client клонируется дёшево, поэтому его можно отдавать в задачи. Запускать сотню запросов одновременно не стоит — получите 429. buffer_unordered(3) держит не больше трёх запросов в полёте:
use async_openai::{
config::OpenAIConfig,
error::OpenAIError,
types::chat::{ChatCompletionRequestUserMessage, CreateChatCompletionRequestArgs},
Client,
};
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = OpenAIConfig::new()
.with_api_base("https://api.zukko.pro/v1")
.with_api_key(std::env::var("ZUKKO_API_KEY")?);
let client = Client::with_config(config);Результаты приходят в порядке завершения, а не в порядке вопросов, поэтому вопрос возвращается вместе с ответом.
Обработка ошибок
Ошибки API приходят как OpenAIError::ApiError(ApiErrorResponse): внутри status_code (HTTP-статус) и api_error с полем message. Сетевые проблемы — OpenAIError::Reqwest, ответ не в ожидаемом формате — OpenAIError::JSONDeserialize вместе с исходным текстом.
match client.chat().create(request).await {
Ok(response) => {
if let Some(choice) = response.choices.first() {
println!("{}", choice.message.content.as_deref().unwrap_or(""));
}
}
Err(OpenAIError::ApiError(e)) => match e.status_code.as_u16() {
401 => eprintln!("Неверный ключ"),
402 => eprintln!("Не хватает баланса"),
403 => eprintln!("Модель не разрешена для этого ключа"),
429 => eprintln!("Слишком частые запросы, повторите позже"),
503 => eprintln!("Провайдер недоступен: {}", e.api_error.message),
code => eprintln!("Ошибка API {code}: {}", e.api_error.message),
},
Err(e) => eprintln!("Ошибка: {e}"),
}Для 429 и 503 разумно повторять запрос с паузой, для 401–403 — нет: повтор ничего не изменит.
Модели и стоимость
Модель задаётся строкой в .model(...). Цены в Zukko за 1M токенов (вход/выход): claude-sonnet-5 и claude-opus-5-5 — $0.30 / $0.30, gpt-5.6-sol — $0.30 / $0.30, gpt-5.6-luna — $0.10 / $0.10, glm-5.3-flash — $0.04 / $0.04. Актуальный список — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете.
Пример: CLI-утилита делает 5 000 запросов в месяц по 1 500 входных и 500 выходных токенов. На claude-sonnet-5: 5 000 × (1 500 + 500) × $0.30 / 1 000 000 = $3.00. На glm-5.3-flash: 5 000 × 2 000 × $0.04 / 1 000 000 = $0.40. Какая модель справляется с вашей задачей, проверьте на своих данных: прогоните одинаковый набор запросов через две модели и сравните результаты.
Частые вопросы
Ошибка «no method named chat found»
Не включён feature-флаг. Добавьте features = ["chat-completion"] в зависимость async-openai. Если нужны все API крейта, есть флаг full, но он заметно увеличивает время сборки.
Получаю 404 Not Found
Проверьте with_api_base: адрес должен заканчиваться на /v1 — https://api.zukko.pro/v1. Крейт сам добавляет /chat/completions.
Можно ли передать параметры, которых нет в типах крейта?
Да. С флагом byot у методов появляются варианты с суффиксом _byot, например client.chat().create_byot(json!({...})), — они принимают произвольный JSON. Это описано в документации крейта в разделе «Bring Your Own Types».
Как вести диалог с историей?
Храните Vec<ChatCompletionRequestMessage>, добавляйте в него реплики пользователя и ответы модели (ChatCompletionRequestAssistantMessage::from(...)) и передавайте весь вектор в .messages(...). API ничего не помнит между запросами, а история оплачивается как входные токены, поэтому старые реплики обрезайте. Подробнее о формате запроса — в справочнике chat completions.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.