Lenguaje natural a comando de shell con una API de LLM: JSON y coste
Un script de Python que pide a un modelo barato un comando de shell como JSON, rechaza o marca las peticiones dudosas, nunca ejecuta nada y pone precio a 1.000 peticiones.

En esta página
Pide al modelo un único objeto JSON con tres campos (type, command, explanation), limita la respuesta a un par de cientos de tokens e imprime el comando en lugar de ejecutarlo. En tokens, una petición así es sobre todo prompt con una respuesta corta y predecible: bajo los supuestos de abajo, 1.000 de ellas cuestan $0.012 en DeepSeek-V4-Flash.
Supón que estás añadiendo un cuadro de "describe lo que quieres" a una CLI interna. Alguien escribe encuentra archivos de más de 100 MB en mi carpeta personal, y quieres de vuelta una línea que pueda leer, comprobar y pegar. Se plantean dos preguntas: cómo evitas que el modelo adivine o haga algo peligroso, y cuánto cuesta esto cuando cien personas lo usan todo el día.
Una tarea acotada: por qué la respuesta es corta#
El 9 de noviembre de 2026 un canal de Telegram recontó la model card de dale-0.8b, un modelo abierto que hace exactamente este único trabajo. Según la model card en Hugging Face (consultada el 10 de noviembre de 2026), dale-0.8b es un fine-tune completo de Qwen3.5-0.8B bajo Apache-2.0 que convierte una petición en español o inglés en un único comando bash de Linux. El autor, Manuel García, reporta un 79,4% (374 de 471 casos) en su propio conjunto de test, frente al 62,8% de Qwen3.5-9B base y de Gemma 4 E4B-it sin fine-tuning. Son cifras autoinformadas sobre el conjunto de test de una persona, así que tómalas como un dato, no como un ranking.
Lo que nos resulta útil es la forma de la tarea, no el modelo. La card describe un system prompt fijo, una línea de contexto (SO, distro, gestor de paquetes, herramientas) más la petición, el thinking desactivado y una respuesta limitada a 200 tokens. Ese límite es una de las dos palancas del coste: cuando la respuesta es un objeto JSON corto, el lado de la salida es corto y predecible, porque el límite lo acota. La otra palanca es el prompt. Qué parte de la factura aporta cada lado lo muestra la tabla de abajo; varía de un modelo a otro.
No necesitas un modelo de 0,8B para eso. El mismo formato funciona en cualquier modelo de API, y eso es lo que construye el resto de este artículo.

El formato de respuesta: comando, rechazo o duda#
El truco es que el modelo siempre tiene una forma legítima de decir "no". La respuesta es un objeto:
{"type": "command | refuse | unsure", "command": "...", "explanation": "..."}command: un comando bash, más una explicación de una frase sobre lo que hace.refuse: la petición es destructiva o no tiene nada que ver con el shell. Sin comando.unsure: la petición es demasiado vaga ("limpia mi disco"). La explicación contiene una pregunta aclaratoria.
Sin unsure, una petición vaga obliga a adivinar, y un rm adivinado es el que se recuerda. El formato de la card tiene los mismos tres tipos; reutilizamos la idea, no el modelo.
Emparejar los pares
Empareja cada tipo de respuesta con lo que debe hacer el script
Toca un elemento de la izquierda, luego su par
- commandMuestra el comando y la explicación; la persona lo copia y lo ejecuta por su cuenta
- refuseImprime la explicación y no ofrece nada que ejecutar
- unsureImprime la pregunta y deja que el usuario la reformule
El script#
Un archivo, el paquete openai, la clave en una variable de entorno. La URL base y el id completo del modelo vienen de la documentación de CheapTokens; los ids cortos no se aceptan, así que usa 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')Ejecútalo como python shellhelp.. Aquí importan tres decisiones:
- Ningún
subprocessen ningún sitio. El comando es texto en pantalla. Ejecutarlo es decisión tuya, después de leerlo. max_tokens=200, igual que el límite de la card. Un objeto JSON cabe de sobra; una respuesta divagante se corta a los 200 tokens en lugar de seguir a tu costa. El límite se aplica a toda la respuesta, así que si un modelo escribe su razonamiento, eso puede comerse el límite. Si alguna respuesta termina confinish_reasonigual alength, sube un poco el límite. Más sobre esto en recortar tokens de salida con max_tokens.- El formato se pide en el prompt y luego se parsea de forma defensiva.
raw_decodese salta un bloque de código suelto o una frase antes del{. Si quieres ir más allá, consulta JSON válido desde una API compatible con OpenAI y la solución para json.JSONDecodeError en una respuesta.
Antes de ejecutar nada, comprueba que el orden de los pasos está claro:
Poner en orden
Pon los pasos del script en el orden correcto
Arrastra los elementos o usa las flechas
- Construir el prompt: texto de sistema fijo, una línea de contexto, la petición
- Llamar a la API con un max_tokens pequeño
- Parsear el JSON y comprobar que type es uno de los tres
- Mostrar el comando y esperar a que la persona lo ejecute
Cuánto cuestan 1.000 peticiones#
La última línea del script imprime usage. Ese es el número real, y deberías leer el tuyo. Hasta que lo tengas, trabaja con un supuesto: el prompt viene a ser unos 400 tokens de entrada (el prompt pelado del script son aproximadamente 150 tokens según un cálculo aproximado; el resto es margen para los tres o cuatro pares de ejemplo petición-respuesta que probablemente añadirías), y la respuesta JSON unos 60 tokens de salida.
Para 1.000 peticiones eso son 400 × 1.000 = 400.000 tokens de entrada y 60 × 1.000 = 60.000 tokens de salida. La tabla pone precio exactamente a eso para cada modelo del catálogo, con la columna de OpenRouter al lado; las cifras vienen de precios en vivo, así que se mantienen al día.
| Modelo | Entrada $/1M | Salida $/1M | Por solicitud | 1000 solicitudes | OpenRouter, 1000 solicitudes |
|---|---|---|---|---|---|
| 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 tokens de entrada y 60 tokens de salida por solicitud. Los precios por 1M de tokens son actuales y cambian junto con nuestras tarifas; la columna de OpenRouter muestra precios de referencia para la misma carga de trabajo.
En total, contando entrada y salida juntas, DeepSeek-V4-Flash sale más barato que OpenRouter para esta carga de trabajo. En tokens la petición es casi toda entrada y la respuesta es corta, pero ambos lados se facturan, así que recorta el prompt y la respuesta por igual. Los precios por modelo están en la página de precios, y el modelo en sí se describe en la página de DeepSeek: 380K de contexto, hasta 32.768 tokens de salida, un único precio a cualquier hora.
Una forma rápida de captar las proporciones:
Verificación rápida
¿Qué parte de la petición limita max_tokens=200?
Mostrar respuesta
Respuesta: La respuesta
max_tokens limita solo la respuesta; el prompt se factura tal cual, así que recórtalo por separado.
¿Quieres 100.000 peticiones al mes? Multiplica por 100: 40.000.000 de entrada y 6.000.000 de salida. Mete esos dos números en ::cost de la misma forma y lee la fila.
Compruébalo con tus propias peticiones#
El 79,4% de la card no dice nada sobre tu prompt, tu SO ni el lenguaje de tus usuarios. Escribe 20 peticiones reales con los comandos que aceptarías y compara, exactamente como en probar un prompt con un conjunto de eval pequeño. Incluye algunas que el script debería rechazar o marcar como dudosas, como "borra todo" o "haz mi ordenador más rápido".
Cuando algo falla, el síntoma suele señalar la solución:
Could not read the reply as JSON: imprime el texto en bruto una vez. Un prefijo como "¡Claro!" se maneja; una respuesta cortada por el límite no, así que revisafinish_reason.401o404: clave, URL base o id de modelo incorrectos. La lista está en API 401 o 404: clave, URL base, id de modelo.- Comandos que encajan con otro sistema: la línea de contexto es la causa. Mantenla precisa (SO, gestor de paquetes, herramientas); cuesta unas pocas docenas de tokens y evita un
aptequivocado en una máquina Fedora.
¿Misma tarea, otro modelo? Cambia MODEL por el id de GLM o MiniMax de la documentación, mantén el prompt y compara los recuentos de tokens que imprime el script con la tabla de arriba.



