Bash, curl и jq: запросы к нейросети Claude прямо из скриптов

Готовая библиотека из трёх функций для вызова Claude из shell: сборка JSON через jq, чтение из пайпа, стриминг без лишних утилит, обработка HTTP-ошибок и пакетная обработка файлов.

5 мин · · разработка · автоматизация

Bash, curl и jq — самый короткий путь подключить нейросеть к существующей автоматизации: разбор логов в cron, описание diff перед коммитом, пакетная обработка текстовых файлов. Zukko принимает запросы в OpenAI-формате на https://api.zukko.pro/v1, поэтому достаточно curl. Ниже — от однострочника до небольшой библиотеки функций; все примеры проверены в bash 3.2 (стоит по умолчанию на macOS) и с jq 1.6.

Однострочник

Ключ fetch_... создайте в кабинете Zukko и задайте переменной окружения:

bash
export ZUKKO_API_KEY=fetch_...

curl -sS https://api.zukko.pro/v1/chat/completions \
  -H "Authorization: Bearer $ZUKKO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-sonnet-5", "max_tokens": 300,
       "messages": [{"role": "user", "content": "Объясни, что делает set -euo pipefail"}]}' \
  | jq -r '.choices[0].message.content'

-sS убирает индикатор прогресса, но оставляет сообщения об ошибках curl. jq -r печатает строку без кавычек и с настоящими переносами строк — без -r вы получите JSON-строку с \n.

Почему JSON нужно собирать через jq

Подставлять переменную прямо в строку -d "{...$prompt...}" нельзя: первая же кавычка, обратный слеш или перенос строки в тексте сломают JSON, а текст из внешнего источника может изменить структуру запроса. Правильно — передать значения в jq -n через --arg (строка), --argjson (число, JSON) или --rawfile (содержимое файла целиком), и jq сам экранирует всё как надо.

Второй момент — длина. Большой текст в аргументе командной строки упрётся в системный лимит длины аргументов. Поэтому тело запроса передаём curl через stdin: --data-binary @-.

Библиотека функций

Сохраните как zukko.sh и подключайте через source zukko.sh:

bash
#!/usr/bin/env bash
set -o pipefail
: "${ZUKKO_API_KEY:?Задайте переменную ZUKKO_API_KEY}"
ZUKKO_URL="https://api.zukko.pro/v1/chat/completions"
ZUKKO_MODEL="${ZUKKO_MODEL:-claude-sonnet-5}"

zukko_request() {
    local tmp code
    tmp=$(mktemp)
    if ! code=$(curl -sS -o "$tmp" -w '%{http_code}' "$ZUKKO_URL" \
        -H "Authorization: Bearer $ZUKKO_API_KEY" \
        -H "Content-Type: application/json" \
        --data-binary @-); then
        rm -f "$tmp"

Что делает каждая функция:

  • zukko_request читает JSON запроса из stdin, отправляет его и печатает ответ. Код ответа получаем через -w '%{http_code}', а тело пишем во временный файл: так не нужен head -n -1, который не работает в BSD-версии head на macOS. При коде не 200 функция пишет в stderr статус и error.message и возвращает 1.
  • ask "вопрос" [max_tokens] — короткий вопрос из аргумента.
  • ask_pipe "задание" — задание из аргумента плюс данные из stdin. Флаг --rawfile data /dev/stdin читает вход целиком, включая переносы строк.
  • ask_stream "вопрос" — печатает ответ по мере генерации.

set -o pipefail нужен, чтобы ошибка zukko_request в середине конвейера не терялась: без него код возврата всей цепочки равен коду последнего jq, то есть почти всегда 0.

Примеры:

bash
source ./zukko.sh

ask "Придумай 5 названий для CLI-утилиты бэкапа"
git diff --staged | ask_pipe "Напиши сообщение коммита в формате Conventional Commits"
tail -n 300 /var/log/nginx/error.log | ask_pipe "Сгруппируй ошибки и предложи причины"
ZUKKO_MODEL=glm-5.3-flash ask "Переведи на английский: доброе утро"

Стриминг без лишних утилит

В режиме "stream": true сервер отвечает строками data: {...} в формате SSE и завершает поток строкой data: [DONE]. Частая ошибка — отдавать этот поток прямо в jq: строки начинаются с data: , и jq падает на разборе. В ask_stream это решено одним вызовом jq:

  • -R читает вход как сырые строки, select(startswith("data: ")) | .[6:] оставляет полезную часть, select(. != "[DONE]") отбрасывает маркер конца, fromjson разбирает JSON;
  • .choices[0]?.delta.content // empty — защита от последнего чанка: Zukko присылает в нём usage и пустой массив choices, и без ? и // empty в выводе появился бы null;
  • -j печатает фрагменты без переводов строк между ними, а --unbuffered выводит их сразу.

curl -N отключает буферизацию на стороне curl, а --fail делает так, что при HTTP-ошибке функция вернёт ненулевой код. Тело ошибки в этом режиме не печатается — если стриминг молчит, повторите тот же запрос через ask, чтобы увидеть сообщение сервера.

Коды ошибок

КодПричинаЧто делать
401неверный ключпроверить ZUKKO_API_KEY
402не хватает балансапополнить баланс в кабинете
403модель не разрешена для ключаизменить allowed_models ключа или модель
413слишком большой запроссократить вход
429слишком частые запросыуменьшить параллельность, повторить позже
503провайдер недоступенповторить позже или взять другую модель из /v1/models

Для 429 и 503 в cron-задачах удобно повторять запрос с паузой, например ask "..." || { sleep 10; ask "..."; }. Для 401–403 повтор бессмыслен.

Пакетная обработка файлов

xargs -P запускает несколько запросов параллельно. Функции нужно экспортировать, иначе дочерний bash -c их не увидит:

bash
set -euo pipefail
source ./zukko.sh
export -f zukko_request ask_pipe
export ZUKKO_URL ZUKKO_MODEL ZUKKO_API_KEY
mkdir -p out

find docs -name '*.md' -print0 \
  | xargs -0 -n 1 -P 4 bash -c 'set -o pipefail; ask_pipe "Перескажи в 5 пунктах:" < "$1" > "out/${1##*/}"' _

Имя файла передаётся в bash -c как позиционный аргумент $1, а не подставляется в строку команды, поэтому пробелы и кавычки в именах ничего не ломают. Каждый ответ пишется в свой файл, так что выводы параллельных запросов не перемешиваются. -P 4 держит не больше четырёх запросов одновременно; если ловите 429, уменьшите число.

Сколько это стоит

Цены в Zukko за 1M токенов (вход/выход): claude-sonnet-5 — $0.30 / $0.30, gpt-5.6-luna — $0.10 / $0.10, glm-5.3-flash — $0.04 / $0.04. Актуальный список моделей:

bash
curl -sS https://api.zukko.pro/v1/models -H "Authorization: Bearer $ZUKKO_API_KEY" | jq -r '.data[].id'

или страница «Модели» в кабинете. Пример: пересказ 200 файлов по 3 000 входных и 300 выходных токенов. На claude-sonnet-5: 200 × (3 000 + 300) × $0.30 / 1 000 000 = $0.198. На glm-5.3-flash: 200 × 3 300 × $0.04 / 1 000 000 = $0.0264. Достаточно ли дешёвой модели для вашей задачи, проверьте на своих данных: прогоните 10 файлов через обе модели и сравните результаты.

Частые вопросы

Как передать многострочный промпт?

Через here-document в подстановке команды:

bash
prompt=$(cat <<'EOF'
Ты ревьюер shell-скриптов.
Найди ошибки в скрипте ниже и предложи исправления.
EOF
)
ask_pipe "$prompt" < deploy.sh

Кавычки вокруг 'EOF' отключают подстановку переменных внутри текста. Конструкция read -d '' для этого хуже: она возвращает ненулевой код и роняет скрипт с set -e.

Почему в ответе видны \n и кавычки?

Не указан jq -r. Без него jq печатает значение как JSON-строку.

Как сохранить результат в переменную?

summary=$(ask_pipe "Кратко:" < report.txt). Если запрос упал, функция вернёт 1, а при set -e скрипт остановится — это удобнее, чем молча записать пустую строку.

Можно ли вызвать Claude в Anthropic-формате через curl?

Да: POST https://api.zukko.pro/v1/messages с заголовком anthropic-version: 2023-06-01, ответ лежит в .content[0].text. Формат запроса описан в справочнике Messages API, а OpenAI-формат — в справочнике chat completions.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.

Bash, curl и jq: запросы к нейросети Claude из скриптов · Zukko