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

Содержание
Попросите модель вернуть один 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, его мы и соберём дальше.

Формат ответа: command, refuse или unsure#
Хитрость в том, что у модели всегда есть законный способ сказать «нет». Ответ — один объект:
{"type": "command | refuse | unsure", "command": "...", "explanation": "..."}command: одна bash-команда и пояснение в одно предложение.refuse: запрос разрушительный или не про shell. Команды нет.unsure: запрос слишком расплывчатый («почисти диск»). Вexplanationлежит уточняющий вопрос.
Без unsure расплывчатый запрос заставляет угадывать, а угаданный rm запоминается надолго. В карточке те же три типа; мы берём идею, а не модель.
Найдите пары
Сопоставьте тип ответа и действие скрипта
Нажмите на элемент слева, затем на его пару
- commandПоказать команду и пояснение; запускает её человек сам
- refuseВывести пояснение и ничего не предлагать запускать
- unsureВывести вопрос и дать пользователю переформулировать
Скрипт#
Один файл, пакет openai, ключ в переменной окружения. Базовый адрес и полный id модели берите из документации CheapTokens; короткие id не принимаются, поэтому здесь deepseek-.
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.. Важны три решения:
- Нигде нет
subprocess. Команда — это текст на экране. Запускать её или нет, решаете вы, после прочтения. max_tokens=200, как лимит в карточке. Один JSON-объект помещается с запасом, а болтливый ответ обрежется на лимите, а не будет расти дальше за ваш счёт. Лимит относится ко всему ответу, поэтому если модель выводит рассуждения, они могут съесть его. Если в ответеfinish_reasonравенlength, чуть поднимите лимит. Подробнее — в статье как сократить выходные токены через max_tokens.- Формат просим в промпте и разбираем с запасом.
raw_decodeпропускает случайный блок кода или фразу перед{. Если хотите надёжнее, смотрите валидный JSON от OpenAI-совместимого API и разбор json.JSONDecodeError на ответе модели.
Прежде чем что-то запускать, проверьте, понятен ли порядок шагов:
Расставьте по порядку
Расставьте шаги скрипта по порядку
Перетащите элементы или используйте стрелки
- Собрать промпт: фиксированный системный текст, строка контекста, запрос
- Вызвать API с небольшим max_tokens
- Разобрать JSON и проверить, что type — один из трёх
- Показать команду и ждать, пока человек запустит её сам
Сколько стоят 1 000 запросов#
Последняя строка скрипта печатает usage. Это настоящее число, и читать нужно своё. Пока его нет, работаем с допущением: промпт даёт около 400 входных токенов (голый промпт из скрипта — по грубой прикидке около 150, остальное — запас на три-четыре примера «запрос → ответ», которые вы, скорее всего, добавите), а JSON-ответ — около 60 выходных.
Для 1 000 запросов это 400 × 1 000 = 400 000 входных и 60 × 1 000 = 60 000 выходных токенов. Таблица считает именно эту нагрузку для каждой модели каталога, рядом — колонка OpenRouter; цифры берутся из живых цен и не устареют.
| Модель | Вход $/1M | Выход $/1M | За запрос | 1 000 запросов | OpenRouter, 1 000 запросов |
|---|---|---|---|---|---|
| GLM-5.3-Flash | $0.02 | $0.04 | $0.00001 | $0.0104 | $0.0184 |
| DeepSeek-V4-Flash | $0.015 | $0.10 | $0.000012 | $0.012 | $0.0364 |
| MiniMax-M2.7 | $0.05 | $0.15 | $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 из документации, оставьте промпт и сравните счётчики токенов из скрипта с таблицей выше.



