Retour au blog

Optimisation des coûtsGuide

Du langage naturel à la commande shell avec une API LLM : JSON et coût

Un script Python qui demande à un modèle bon marché une commande shell en JSON, refuse ou signale les demandes incertaines, n'exécute jamais rien et chiffre 1 000 requêtes.

Par CheapTokens TeamPublié le 7 min de lecture

Une demande en langage courant transformée en une commande shell dans un objet JSON avec les types command, refuse et unsure
Sur cette page
  1. Une tâche étroite : pourquoi la réponse est courte
  2. Le format de réponse : command, refuse ou unsure
  3. Le script
  4. Ce que coûtent 1 000 requêtes
  5. Vérifiez-le sur vos propres requêtes

Demandez au modèle un objet JSON avec trois champs (type, command, explanation), limitez la réponse à quelques centaines de tokens et affichez la commande au lieu de l'exécuter. En tokens, une requête de ce type est surtout du prompt avec une réponse courte et prévisible : dans les hypothèses ci-dessous, 1 000 d'entre elles coûtent $0.012 sur DeepSeek-V4-Flash.

Imaginez que vous ajoutiez une zone « décrivez ce que vous voulez » à un CLI interne. Quelqu'un tape trouve les fichiers de plus de 100 Mo dans mon dossier personnel, et vous voulez en retour une ligne qu'il peut lire, vérifier et coller. Deux questions suivent : comment empêcher le modèle de deviner ou de faire quelque chose de dangereux, et combien cela coûte quand cent personnes l'utilisent toute la journée ?

Une tâche étroite : pourquoi la réponse est courte#

Le 9 novembre 2026, un canal Telegram a relayé la fiche modèle de dale-0.8b, un modèle ouvert qui fait exactement ce seul travail. D'après la fiche modèle sur Hugging Face (vérifiée le 10 novembre 2026), dale-0.8b est un fine-tune complet de Qwen3.5-0.8B sous Apache-2.0 qui transforme une demande en espagnol ou en anglais en une commande bash Linux. L'auteur, Manuel García, rapporte 79,4 % (374 cas sur 471) sur son propre jeu de test, contre 62,8 % pour Qwen3.5-9B base et pour Gemma 4 E4B-it sans fine-tuning. Ce sont des chiffres auto-déclarés sur le jeu de test d'une seule personne : à traiter comme un point de données, pas comme un classement.

Ce qui nous est utile, c'est la forme de la tâche, pas le modèle. La fiche décrit un prompt système fixe, une ligne de contexte (OS, distribution, gestionnaire de paquets, outils) plus la demande, la réflexion désactivée et une réponse plafonnée à 200 tokens. Ce plafond est l'un des deux leviers sur le coût : quand la réponse est un court objet JSON, le côté sortie est court et prévisible, car le plafond le limite. L'autre levier est le prompt. La part de chacun dans la facture, le tableau ci-dessous la montre ; elle varie d'un modèle à l'autre.

Vous n'avez pas besoin d'un modèle 0.8B pour cela. Le même format fonctionne sur n'importe quel modèle d'API, et c'est ce que construit le reste de cet article.

Le parcours d'une requête : prompt en entrée, JSON en sortie, un humain décide
Le flux de la requête dans le script : rien n'est exécuté tant que vous ne le faites pas vous-même.

Le format de réponse : command, refuse ou unsure#

L'astuce, c'est que le modèle a toujours un moyen légal de dire « non ». La réponse est un objet :

json
{"type": "command | refuse | unsure", "command": "...", "explanation": "..."}
  • command : une commande bash, plus une explication en une phrase de ce qu'elle fait.
  • refuse : la demande est destructrice ou n'a rien à voir avec le shell. Pas de commande.
  • unsure : la demande est trop vague (« nettoie mon disque »). L'explication contient une question de clarification.

Sans unsure, une demande vague force à deviner, et un rm deviné est celui dont on se souvient. Le format de la fiche a les mêmes trois types ; on réutilise l'idée, pas le modèle.

Associer les paires

Associez chaque type de réponse à ce que le script doit faire

  • commandAfficher la commande et l'explication ; la personne la copie et l'exécute elle-même
  • refuseAfficher l'explication et ne rien proposer à exécuter
  • unsureAfficher la question et laisser l'utilisateur reformuler

Le script#

Un fichier, le paquet openai, la clé dans une variable d'environnement. L'URL de base et l'id complet du modèle viennent de la doc CheapTokens ; les ids courts ne sont pas acceptés, utilisez donc 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')

Lancez-le avec python shellhelp.py find files bigger than 100 MB in my home folder. Trois choix comptent ici :

  • Aucun subprocess nulle part. La commande est du texte à l'écran. L'exécuter est votre décision, après l'avoir lue.
  • max_tokens=200, comme le plafond de la fiche. Un objet JSON tient facilement ; une réponse qui divague est coupée à 200 tokens au lieu de continuer à vos frais. Le plafond s'applique à toute la réponse, donc si un modèle écrit son raisonnement, cela peut manger la limite. Si une réponse se termine avec finish_reason égal à length, augmentez un peu le plafond. Plus de détails dans réduire les tokens de sortie avec max_tokens.
  • Le format est demandé dans le prompt, puis analysé de façon défensive. raw_decode saute une clôture de code parasite ou une phrase avant le {. Pour aller plus loin, voir du JSON valide depuis une API compatible OpenAI et le correctif pour json.JSONDecodeError sur une réponse.

Avant d'exécuter quoi que ce soit, vérifiez que l'ordre des étapes est clair :

Remettre en ordre

Remettez les étapes du script dans le bon ordre

  1. Construire le prompt : texte système fixe, une ligne de contexte, la demande
  2. Appeler l'API avec un petit max_tokens
  3. Analyser le JSON et vérifier que type est l'un des trois
  4. Afficher la commande et attendre que la personne l'exécute

Ce que coûtent 1 000 requêtes#

La dernière ligne du script affiche usage. C'est le vrai chiffre, et vous devriez lire le vôtre. En attendant de l'avoir, partez d'une hypothèse : le prompt fait environ 400 tokens d'entrée (le prompt nu du script fait à peu près 150 tokens au compteur ; le reste est de la place pour les trois ou quatre paires exemple-demande-réponse que vous ajouteriez probablement), et la réponse JSON environ 60 tokens de sortie.

Pour 1 000 requêtes, cela fait 400 × 1 000 = 400 000 tokens d'entrée et 60 × 1 000 = 60 000 tokens de sortie. Le tableau chiffre exactement cela pour chaque modèle du catalogue, avec la colonne OpenRouter à côté ; les chiffres viennent des prix en direct, donc ils restent à jour.

Modèle Par requête 1 000 requêtes OpenRouter, 1 000 requêtes
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 tokens d'entrée et 60 tokens de sortie par requête. Les prix par million de tokens sont actuels et évoluent avec nos tarifs ; la colonne OpenRouter indique des prix de référence pour la même charge de travail.

Au total, entrée et sortie comptées ensemble, DeepSeek-V4-Flash revient moins cher qu'OpenRouter pour cette charge. En tokens, la requête est presque toute en entrée et la réponse est courte, mais les deux côtés sont facturés : allégez donc le prompt comme la réponse. Les prix par modèle sont sur la page de tarifs, et le modèle lui-même est décrit sur la page DeepSeek : contexte de 380K, jusqu'à 32 768 tokens de sortie, un seul prix à toute heure.

Un moyen rapide de sentir les proportions :

Vérification rapide

Quelle partie de la requête max_tokens=200 limite-t-il ?

Afficher la réponse

Réponse: La réponse

max_tokens ne limite que la réponse ; le prompt est facturé tel quel, allégez-le séparément.

Vous voulez 100 000 requêtes par mois ? Multipliez par 100 : 40 000 000 en entrée et 6 000 000 en sortie. Mettez ces deux nombres dans ::cost de la même façon et lisez la ligne.

Vérifiez-le sur vos propres requêtes#

Les 79,4 % de la fiche ne disent rien de votre prompt, de votre OS ni du vocabulaire de vos utilisateurs. Écrivez 20 vraies requêtes avec les commandes que vous accepteriez et comparez, exactement comme dans tester un prompt sur un petit jeu d'éval. Incluez-en quelques-unes que le script devrait refuser ou signaler comme incertaines, comme « supprime tout » ou « rends mon ordinateur plus rapide ».

Quand quelque chose cloche, le symptôme indique généralement le correctif :

  • Could not read the reply as JSON : affichez une fois le texte brut. Un préfixe comme « Bien sûr ! » est géré ; une réponse coupée par le plafond ne l'est pas, vérifiez donc finish_reason.
  • 401 ou 404 : mauvaise clé, URL de base ou id de modèle. La liste est dans API 401 ou 404 : clé, URL de base, id de modèle.
  • Des commandes adaptées à un autre système : la ligne de contexte en est la cause. Gardez-la exacte (OS, gestionnaire de paquets, outils) ; elle coûte quelques dizaines de tokens et évite un mauvais apt sur une machine Fedora.

Même tâche, autre modèle ? Remplacez MODEL par l'id GLM ou MiniMax de la doc, gardez le prompt, et comparez les comptes de tokens affichés par le script avec le tableau ci-dessus.