Назад в блог

Оптимизация расходовИнструкция

Запрос в shell-команду через LLM API: JSON-ответ и стоимость

Python-скрипт: дешёвая модель возвращает одну shell-команду в JSON, отказывает или сомневается, ничего не запускает; плюс цена 1 000 запросов.

Автор: CheapTokens TeamОпубликовано 5 мин чтения

Запрос на обычном языке превращается в одну shell-команду в JSON-объекте с типами command, refuse и unsure
Содержание
  1. Узкая задача: почему ответ короткий
  2. Формат ответа: command, refuse или unsure
  3. Скрипт
  4. Сколько стоят 1 000 запросов
  5. Проверьте на своих запросах

Попросите модель вернуть один JSON-объект из трёх полей (type, command, explanation), ограничьте ответ парой сотен токенов и выводите команду на экран, а не запускайте. По токенам такой запрос — это почти целиком промпт и короткий предсказуемый ответ: при допущениях ниже 1 000 запросов на DeepSeek-V4-Flash стоят $0.012.

Допустим, вы добавляете во внутренний CLI поле «опишите, что нужно». Человек пишет найти файлы больше 100 МБ в домашней папке, а вам нужна одна строка, которую можно прочитать, проверить и вставить. Отсюда два вопроса: как не дать модели гадать и предлагать опасное, и сколько это стоит, когда сотня человек пользуется им весь день.

Узкая задача: почему ответ короткий#

9 ноября 2026 года Telegram-канал пересказал карточку модели dale-0.8b — открытой модели, которая делает ровно одно такое дело. Согласно карточке на Hugging Face (проверено 10 ноября 2026), dale-0.8b — полный fine-tune Qwen3.5-0.8B под Apache-2.0, превращающий запрос на испанском или английском в одну bash-команду Linux. Автор, Manuel García, сообщает 79,4% (374 из 471 случая) на собственном наборе тестов против 62,8% у базовой Qwen3.5-9B и у Gemma 4 E4B-it без дообучения. Это его собственные цифры на одном наборе, так что воспринимайте их как точку данных, а не как рейтинг.

Нам важна не сама модель, а форма задачи. В карточке описаны фиксированный системный промпт, строка контекста (ОС, дистрибутив, менеджер пакетов, утилиты) плюс запрос, выключенное «мышление» и ответ с лимитом 200 токенов. Этот лимит — один из двух рычагов стоимости: когда ответ — один короткий JSON, выходная часть короткая и предсказуемая, потому что её ограничивает лимит. Второй рычаг — промпт. Какую долю счёта даёт каждая сторона, видно в таблице ниже: у разных моделей она разная.

Модель на 0,8 млрд параметров для этого не нужна. Тот же формат работает с любой моделью по API, его мы и соберём дальше.

Путь одного запроса: промпт на входе, JSON на выходе, решает человек
Поток запроса в скрипте: пока вы сами не запустите команду, ничего не выполняется.

Формат ответа: command, refuse или unsure#

Хитрость в том, что у модели всегда есть законный способ сказать «нет». Ответ — один объект:

json
{"type": "command | refuse | unsure", "command": "...", "explanation": "..."}
  • command: одна bash-команда и пояснение в одно предложение.
  • refuse: запрос разрушительный или не про shell. Команды нет.
  • unsure: запрос слишком расплывчатый («почисти диск»). В explanation лежит уточняющий вопрос.

Без unsure расплывчатый запрос заставляет угадывать, а угаданный rm запоминается надолго. В карточке те же три типа; мы берём идею, а не модель.

Найдите пары

Сопоставьте тип ответа и действие скрипта

  • commandПоказать команду и пояснение; запускает её человек сам
  • refuseВывести пояснение и ничего не предлагать запускать
  • unsureВывести вопрос и дать пользователю переформулировать

Скрипт#

Один файл, пакет openai, ключ в переменной окружения. Базовый адрес и полный id модели берите из документации CheapTokens; короткие id не принимаются, поэтому здесь deepseek-ai/DeepSeek-V4-Flash-0731.

python
import json
import os
import sys

from openai import OpenAI

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

SYSTEM = '''You turn a request into ONE Linux bash command.
Reply with a single JSON object and nothing else:
{"type": "command" | "refuse" | "unsure", "command": "<one command, or empty>", "explanation": "<one short sentence>"}
Use "refuse" when the request is destructive or not about the shell.
Use "unsure" when it is too vague, and put a clarifying question in "explanation".'''

CONTEXT = 'os: Ubuntu 24.04; package manager: apt; tools: git, curl, jq'


def parse(text):
    start = text.find('{')
    if start == -1:
        return None
    try:
        obj, _ = json.JSONDecoder().raw_decode(text[start:])
    except json.JSONDecodeError:
        return None
    if not isinstance(obj, dict) or obj.get('type') not in ('command', 'refuse', 'unsure'):
        return None
    return obj


def ask(request):
    resp = client.chat.completions.create(
        model=MODEL,
        max_tokens=200,
        temperature=0,
        messages=[
            {'role': 'system', 'content': SYSTEM},
            {'role': 'user', 'content': f'Context: {CONTEXT}\nRequest: {request}'},
        ],
    )
    return parse(resp.choices[0].message.content), resp.usage


if __name__ == '__main__':
    result, usage = ask(' '.join(sys.argv[1:]))
    if result is None:
        print('Could not read the reply as JSON. Try again.')
    elif result['type'] == 'command':
        print(result['explanation'])
        print('\n    ' + result['command'] + '\n')
        print('Read it, then copy it yourself. This script never runs it.')
    else:
        print(f"[{result['type']}] {result['explanation']}")
    print(f'tokens: {usage.prompt_tokens} in, {usage.completion_tokens} out')

Запуск: python shellhelp.py find files bigger than 100 MB in my home folder. Важны три решения:

  • Нигде нет subprocess. Команда — это текст на экране. Запускать её или нет, решаете вы, после прочтения.
  • max_tokens=200, как лимит в карточке. Один JSON-объект помещается с запасом, а болтливый ответ обрежется на лимите, а не будет расти дальше за ваш счёт. Лимит относится ко всему ответу, поэтому если модель выводит рассуждения, они могут съесть его. Если в ответе finish_reason равен length, чуть поднимите лимит. Подробнее — в статье как сократить выходные токены через max_tokens.
  • Формат просим в промпте и разбираем с запасом. raw_decode пропускает случайный блок кода или фразу перед {. Если хотите надёжнее, смотрите валидный JSON от OpenAI-совместимого API и разбор json.JSONDecodeError на ответе модели.

Прежде чем что-то запускать, проверьте, понятен ли порядок шагов:

Расставьте по порядку

Расставьте шаги скрипта по порядку

  1. Собрать промпт: фиксированный системный текст, строка контекста, запрос
  2. Вызвать API с небольшим max_tokens
  3. Разобрать JSON и проверить, что type — один из трёх
  4. Показать команду и ждать, пока человек запустит её сам

Сколько стоят 1 000 запросов#

Последняя строка скрипта печатает usage. Это настоящее число, и читать нужно своё. Пока его нет, работаем с допущением: промпт даёт около 400 входных токенов (голый промпт из скрипта — по грубой прикидке около 150, остальное — запас на три-четыре примера «запрос → ответ», которые вы, скорее всего, добавите), а JSON-ответ — около 60 выходных.

Для 1 000 запросов это 400 × 1 000 = 400 000 входных и 60 × 1 000 = 60 000 выходных токенов. Таблица считает именно эту нагрузку для каждой модели каталога, рядом — колонка OpenRouter; цифры берутся из живых цен и не устареют.

Модель За запрос 1 000 запросов OpenRouter, 1 000 запросов
GLM-5.3-Flash $0.00001 $0.0104 $0.0184
DeepSeek-V4-Flash $0.000012 $0.012 $0.0364
MiniMax-M2.7 $0.000029 $0.029 $0.348

400 входных и 60 выходных токенов на запрос. Цены за 1M токенов актуальны и меняются вместе с нашими тарифами; столбец OpenRouter показывает справочные цены для той же нагрузки.

В сумме, с учётом входа и выхода, DeepSeek-V4-Flash для этой нагрузки выходит дешевле OpenRouter. По токенам запрос — почти целиком вход, а ответ короткий, но оплачиваются обе стороны, поэтому сокращайте и промпт, и ответ. Цены по моделям — на странице цен, а о самой модели — на странице моделей: контекст 380K, до 32 768 токенов в ответе, одна цена в любое время суток.

Чтобы почувствовать пропорции:

Быстрая проверка

Что ограничивает max_tokens=200?

Показать ответ

Ответ: Ответ

max_tokens ограничивает только ответ; промпт оплачивается как есть, поэтому сокращайте его отдельно.

Нужно 100 000 запросов в месяц? Умножьте на 100: 40 000 000 входных и 6 000 000 выходных токенов. Подставьте эти два числа в ::cost так же и прочитайте строку.

Проверьте на своих запросах#

79,4% из карточки ничего не говорят о вашем промпте, вашей ОС и формулировках ваших пользователей. Напишите 20 настоящих запросов с командами, которые вы бы приняли, и сравните — как в статье проверка промпта на небольшом наборе примеров. Добавьте несколько таких, где скрипт должен отказать или усомниться: «удали всё», «ускорь мой компьютер».

Если что-то не работает, симптом обычно подсказывает причину:

  • Could not read the reply as JSON: один раз напечатайте сырой текст. Префикс вроде «Конечно!» обработается; ответ, обрезанный лимитом, нет, поэтому смотрите finish_reason.
  • 401 или 404: неверный ключ, адрес или id модели. Разбор — в статье API 401 или 404: ключ, base URL, id модели.
  • Команды под другую систему: виновата строка контекста. Держите её точной (ОС, менеджер пакетов, утилиты); она стоит несколько десятков токенов и бережёт от лишнего apt на Fedora.

Та же задача, другая модель? Замените MODEL на id GLM или MiniMax из документации, оставьте промпт и сравните счётчики токенов из скрипта с таблицей выше.