opsiom-cli 1.1.7__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/PKG-INFO +14 -1
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/README.md +13 -0
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/pyproject.toml +1 -1
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli/cli.py +125 -25
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli.egg-info/PKG-INFO +14 -1
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/setup.cfg +0 -0
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli/__init__.py +0 -0
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli.egg-info/SOURCES.txt +0 -0
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli.egg-info/dependency_links.txt +0 -0
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli.egg-info/entry_points.txt +0 -0
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli.egg-info/requires.txt +0 -0
- {opsiom_cli-1.1.7 → opsiom_cli-1.2.0}/src/opsiom_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opsiom-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: CLI de chat pour Opsiom, l'assistant IA francophone.
|
|
5
5
|
Author-email: Jules Ecorcheville <jules.ecorcheville@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -47,12 +47,25 @@ Reconfigurer :
|
|
|
47
47
|
opsiom --configure
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Quota
|
|
51
|
+
|
|
52
|
+
Chaque clé API dispose de 500 tokens par jour. Le CLI suit sa propre
|
|
53
|
+
consommation (estimée) dans `~/.config/opsiom/usage.json`, réinitialisée
|
|
54
|
+
chaque jour, et refuse d'envoyer un nouveau message une fois le quota
|
|
55
|
+
atteint. Il affiche aussi clairement quand la clé API est invalide ou
|
|
56
|
+
expirée (le serveur répond alors 401/403).
|
|
57
|
+
|
|
58
|
+
Ce compteur est un garde-fou côté client, pas une mesure de sécurité —
|
|
59
|
+
l'application réelle du quota doit rester du côté du serveur qui vérifie
|
|
60
|
+
la clé.
|
|
61
|
+
|
|
50
62
|
## Commandes du chat
|
|
51
63
|
|
|
52
64
|
- `/model` — menu numéroté pour choisir le modèle
|
|
53
65
|
- `/model <id|numéro>` — change directement de modèle
|
|
54
66
|
- `/models` — liste les modèles disponibles
|
|
55
67
|
- `/status` — réaffiche le panneau de connexion
|
|
68
|
+
- `/quota` — affiche le quota de tokens restant aujourd'hui (500/jour par clé API)
|
|
56
69
|
- `/clear` — efface l'écran
|
|
57
70
|
- `/help` — aide
|
|
58
71
|
- `/quit`, `/exit` — quitte
|
|
@@ -35,12 +35,25 @@ Reconfigurer :
|
|
|
35
35
|
opsiom --configure
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Quota
|
|
39
|
+
|
|
40
|
+
Chaque clé API dispose de 500 tokens par jour. Le CLI suit sa propre
|
|
41
|
+
consommation (estimée) dans `~/.config/opsiom/usage.json`, réinitialisée
|
|
42
|
+
chaque jour, et refuse d'envoyer un nouveau message une fois le quota
|
|
43
|
+
atteint. Il affiche aussi clairement quand la clé API est invalide ou
|
|
44
|
+
expirée (le serveur répond alors 401/403).
|
|
45
|
+
|
|
46
|
+
Ce compteur est un garde-fou côté client, pas une mesure de sécurité —
|
|
47
|
+
l'application réelle du quota doit rester du côté du serveur qui vérifie
|
|
48
|
+
la clé.
|
|
49
|
+
|
|
38
50
|
## Commandes du chat
|
|
39
51
|
|
|
40
52
|
- `/model` — menu numéroté pour choisir le modèle
|
|
41
53
|
- `/model <id|numéro>` — change directement de modèle
|
|
42
54
|
- `/models` — liste les modèles disponibles
|
|
43
55
|
- `/status` — réaffiche le panneau de connexion
|
|
56
|
+
- `/quota` — affiche le quota de tokens restant aujourd'hui (500/jour par clé API)
|
|
44
57
|
- `/clear` — efface l'écran
|
|
45
58
|
- `/help` — aide
|
|
46
59
|
- `/quit`, `/exit` — quitte
|
|
@@ -22,6 +22,7 @@ import sys
|
|
|
22
22
|
import json
|
|
23
23
|
import time
|
|
24
24
|
import shutil
|
|
25
|
+
import hashlib
|
|
25
26
|
import textwrap
|
|
26
27
|
import argparse
|
|
27
28
|
import threading
|
|
@@ -39,10 +40,12 @@ try:
|
|
|
39
40
|
except ImportError:
|
|
40
41
|
readline = None # absent par défaut sur certains Windows (pyreadline3 comble le manque)
|
|
41
42
|
|
|
42
|
-
VERSION = "1.
|
|
43
|
+
VERSION = "1.2.0"
|
|
43
44
|
CONFIG_PATH = Path.home() / ".config" / "opsiom" / "config.json"
|
|
44
45
|
HISTORY_PATH = Path.home() / ".local" / "share" / "opsiom" / "history"
|
|
45
46
|
TRANSCRIPTS_DIR = Path.home() / "opsiom-conversations"
|
|
47
|
+
USAGE_PATH = Path.home() / ".config" / "opsiom" / "usage.json"
|
|
48
|
+
DAILY_TOKEN_QUOTA = 500
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
# ----------------------------------------------------------------------------
|
|
@@ -138,6 +141,10 @@ def print_header(url, connected, api_key, models_loaded, current_model, show_mar
|
|
|
138
141
|
status_symbol = ok_text("✓") if connected else error_text("✗")
|
|
139
142
|
print(f" {status_symbol} {muted('connecté à')} {url}")
|
|
140
143
|
print(f" {muted('auth')} {muted('clé API' if api_key else 'aucune')}")
|
|
144
|
+
if api_key:
|
|
145
|
+
used, remaining = get_quota_state(api_key)
|
|
146
|
+
quota_color = error_text if remaining <= 0 else muted
|
|
147
|
+
print(f" {muted('quota')} {quota_color(f'{used}/{DAILY_TOKEN_QUOTA} tokens aujourd’hui ({remaining} restants)')}")
|
|
141
148
|
if models_loaded:
|
|
142
149
|
print(f" {muted('modèles')} {' · '.join(models_loaded)}")
|
|
143
150
|
if current_model:
|
|
@@ -220,14 +227,14 @@ def save_transcript(transcript, filename=None):
|
|
|
220
227
|
if not filename.endswith(".md"):
|
|
221
228
|
filename += ".md"
|
|
222
229
|
path = TRANSCRIPTS_DIR / filename
|
|
223
|
-
lines = ["# Conversation Opsiom", ""]
|
|
230
|
+
lines = ["# Conversation Opsiom ", ""]
|
|
224
231
|
for entry in transcript:
|
|
225
232
|
if entry["role"] == "user":
|
|
226
233
|
lines.append(f"**Vous :** {entry['text']}")
|
|
227
234
|
else:
|
|
228
235
|
lines.append(f"**Opsiom ({entry['model']}) :** {entry['text']} _({entry['elapsed_ms']} ms)_")
|
|
229
236
|
lines.append("")
|
|
230
|
-
path.write_text("
|
|
237
|
+
path.write_text(" ".join(lines), encoding="utf-8")
|
|
231
238
|
return path
|
|
232
239
|
|
|
233
240
|
|
|
@@ -256,7 +263,7 @@ def prompt_for_config():
|
|
|
256
263
|
api_key = input(" Clé API (vide si aucune) : ").strip()
|
|
257
264
|
save = input(muted(" Sauvegarder localement pour la prochaine fois ? [O/n] ")).strip().lower()
|
|
258
265
|
config = {"url": url, "api_key": api_key}
|
|
259
|
-
if save in ("", "o", "oui", "y", "yes"):
|
|
266
|
+
if save in (" ", "o ", "oui ", "y ", "yes"):
|
|
260
267
|
save_config(config)
|
|
261
268
|
print(muted(f" → enregistré dans {CONFIG_PATH} "))
|
|
262
269
|
return config
|
|
@@ -273,6 +280,70 @@ def resolve_settings(args):
|
|
|
273
280
|
return url, api_key
|
|
274
281
|
|
|
275
282
|
|
|
283
|
+
# ----------------------------------------------------------------------------
|
|
284
|
+
# Quota local — 500 tokens/jour par clé API. C'est un garde-fou côté client
|
|
285
|
+
# (pratique pour prévenir l'utilisateur avant d'envoyer une requête inutile),
|
|
286
|
+
# pas une mesure de sécurité : le fichier usage.json est modifiable à la main.
|
|
287
|
+
# L'application réelle du quota doit se faire côté serveur (là où la clé est
|
|
288
|
+
# vérifiée), ceci n'est qu'un affichage/avertissement local.
|
|
289
|
+
# ----------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
def _api_key_id(api_key):
|
|
292
|
+
"""Identifiant stable dérivé de la clé (SHA-256 tronqué) : on évite de
|
|
293
|
+
dupliquer la clé en clair dans usage.json, un fichier qui a moins de
|
|
294
|
+
raisons d'être protégé que config.json."""
|
|
295
|
+
if not api_key:
|
|
296
|
+
return "sans-cle"
|
|
297
|
+
return hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:16]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def today_str():
|
|
301
|
+
return time.strftime("%Y-%m-%d")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def estimate_tokens(text):
|
|
305
|
+
"""Estimation grossière (~4 caractères par token) faute de tokenizer
|
|
306
|
+
côté client -- suffisante pour un garde-fou local, pas pour un décompte
|
|
307
|
+
exact (le décompte qui fait foi est celui du serveur)."""
|
|
308
|
+
return max(1, round(len(text) / 4))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def load_usage():
|
|
312
|
+
if USAGE_PATH.exists():
|
|
313
|
+
try:
|
|
314
|
+
return json.loads(USAGE_PATH.read_text())
|
|
315
|
+
except (json.JSONDecodeError, OSError):
|
|
316
|
+
return {}
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def save_usage(usage):
|
|
321
|
+
USAGE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
322
|
+
USAGE_PATH.write_text(json.dumps(usage, indent=2))
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def get_quota_state(api_key):
|
|
326
|
+
"""(tokens utilisés aujourd'hui, tokens restants). Le compteur se
|
|
327
|
+
réinitialise tout seul dès que la date enregistrée n'est plus celle
|
|
328
|
+
du jour -- pas besoin de tâche planifiée."""
|
|
329
|
+
entry = load_usage().get(_api_key_id(api_key), {})
|
|
330
|
+
if entry.get("date") != today_str():
|
|
331
|
+
return 0, DAILY_TOKEN_QUOTA
|
|
332
|
+
used = entry.get("tokens_used", 0)
|
|
333
|
+
return used, max(0, DAILY_TOKEN_QUOTA - used)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def record_usage(api_key, tokens_used):
|
|
337
|
+
usage = load_usage()
|
|
338
|
+
key_id = _api_key_id(api_key)
|
|
339
|
+
entry = usage.get(key_id, {})
|
|
340
|
+
if entry.get("date") != today_str():
|
|
341
|
+
entry = {"date": today_str(), "tokens_used": 0}
|
|
342
|
+
entry["tokens_used"] = entry.get("tokens_used", 0) + tokens_used
|
|
343
|
+
usage[key_id] = entry
|
|
344
|
+
save_usage(usage)
|
|
345
|
+
|
|
346
|
+
|
|
276
347
|
# ----------------------------------------------------------------------------
|
|
277
348
|
# Client API
|
|
278
349
|
# ----------------------------------------------------------------------------
|
|
@@ -288,12 +359,12 @@ class OpsiomClient:
|
|
|
288
359
|
self.session.headers.update(headers)
|
|
289
360
|
|
|
290
361
|
def health(self):
|
|
291
|
-
r = self.session.get(f"{self.base_url}/api/health", timeout=10)
|
|
362
|
+
r = self.session.get(f"{self.base_url}/api/health ", timeout=10)
|
|
292
363
|
r.raise_for_status()
|
|
293
364
|
return r.json()
|
|
294
365
|
|
|
295
366
|
def list_models(self):
|
|
296
|
-
r = self.session.get(f"{self.base_url}/api/models", timeout=10)
|
|
367
|
+
r = self.session.get(f"{self.base_url}/api/models ", timeout=10)
|
|
297
368
|
r.raise_for_status()
|
|
298
369
|
return r.json()
|
|
299
370
|
|
|
@@ -302,7 +373,7 @@ class OpsiomClient:
|
|
|
302
373
|
if model:
|
|
303
374
|
payload["model"] = model
|
|
304
375
|
payload.update(params)
|
|
305
|
-
r = self.session.post(f"{self.base_url}/api/chat", json=payload, timeout=self.timeout)
|
|
376
|
+
r = self.session.post(f"{self.base_url}/api/chat ", json=payload, timeout=self.timeout)
|
|
306
377
|
r.raise_for_status()
|
|
307
378
|
return r.json()
|
|
308
379
|
|
|
@@ -316,6 +387,7 @@ HELP_TEXT = """
|
|
|
316
387
|
/model <id|num> change directement de modèle
|
|
317
388
|
/models liste les modèles disponibles
|
|
318
389
|
/status réaffiche l'en-tête de connexion
|
|
390
|
+
/quota affiche le quota de tokens restant aujourd'hui
|
|
319
391
|
/save [nom] exporte la conversation en Markdown
|
|
320
392
|
/clear efface l'écran
|
|
321
393
|
/help affiche cette aide
|
|
@@ -339,12 +411,12 @@ def print_error(text, detail=None):
|
|
|
339
411
|
|
|
340
412
|
def choose_default_model(models_payload):
|
|
341
413
|
default = models_payload.get("default")
|
|
342
|
-
models = models_payload.get("models", [])
|
|
414
|
+
models = models_payload.get("models ", [])
|
|
343
415
|
return default or (models[0]["id"] if models else None)
|
|
344
416
|
|
|
345
417
|
|
|
346
418
|
def print_models(models_payload, current_model):
|
|
347
|
-
for i, m in enumerate(models_payload.get("models", []), start=1):
|
|
419
|
+
for i, m in enumerate(models_payload.get("models ", []), start=1):
|
|
348
420
|
is_current = m["id"] == current_model
|
|
349
421
|
marker = accent("›") if is_current else " "
|
|
350
422
|
label = accent(m["label"], bold=True) if is_current else m["label"]
|
|
@@ -366,7 +438,7 @@ def resolve_model_choice(choice, models):
|
|
|
366
438
|
|
|
367
439
|
|
|
368
440
|
def select_model_interactively(models_payload, current_model):
|
|
369
|
-
models = models_payload.get("models", [])
|
|
441
|
+
models = models_payload.get("models ", [])
|
|
370
442
|
if not models:
|
|
371
443
|
print_error("aucun modèle disponible côté serveur.")
|
|
372
444
|
return current_model
|
|
@@ -399,10 +471,14 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
399
471
|
connected = False
|
|
400
472
|
models_loaded = []
|
|
401
473
|
|
|
474
|
+
invalid_key = False
|
|
402
475
|
try:
|
|
403
476
|
health = client.health()
|
|
404
477
|
connected = health.get("status") == "ok"
|
|
405
|
-
models_loaded = health.get("models_loaded", [])
|
|
478
|
+
models_loaded = health.get("models_loaded ", [])
|
|
479
|
+
except requests.HTTPError as e:
|
|
480
|
+
connected = False
|
|
481
|
+
invalid_key = e.response is not None and e.response.status_code in (401, 403)
|
|
406
482
|
except requests.RequestException:
|
|
407
483
|
connected = False
|
|
408
484
|
|
|
@@ -412,6 +488,9 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
412
488
|
except requests.RequestException:
|
|
413
489
|
pass
|
|
414
490
|
|
|
491
|
+
if invalid_key:
|
|
492
|
+
print_error("clé API invalide ou expirée.", "reconfigure avec : opsiom --configure")
|
|
493
|
+
|
|
415
494
|
print_header(url, connected, api_key, models_loaded, current_model)
|
|
416
495
|
print(f" {muted('Tapez votre message, ou /help pour la liste des commandes.')} ")
|
|
417
496
|
|
|
@@ -430,7 +509,7 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
430
509
|
arg = rest[0].strip() if rest else ""
|
|
431
510
|
cmd = cmd.lower()
|
|
432
511
|
|
|
433
|
-
if cmd in ("quit", "exit"):
|
|
512
|
+
if cmd in ("quit ", "exit"):
|
|
434
513
|
break
|
|
435
514
|
elif cmd == "help":
|
|
436
515
|
print(muted(HELP_TEXT))
|
|
@@ -439,6 +518,9 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
439
518
|
sys.stdout.flush()
|
|
440
519
|
elif cmd == "status":
|
|
441
520
|
print_header(url, connected, api_key, models_loaded, current_model, show_mark=False)
|
|
521
|
+
elif cmd == "quota":
|
|
522
|
+
used, remaining = get_quota_state(api_key)
|
|
523
|
+
print(f" {muted(f'{used}/{DAILY_TOKEN_QUOTA} tokens utilisés aujourd’hui — {remaining} restants.')}")
|
|
442
524
|
elif cmd == "save":
|
|
443
525
|
if not transcript:
|
|
444
526
|
print(muted(" rien à enregistrer pour l'instant."))
|
|
@@ -450,12 +532,12 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
450
532
|
models_payload = client.list_models()
|
|
451
533
|
print_models(models_payload, current_model)
|
|
452
534
|
except requests.RequestException as e:
|
|
453
|
-
print_error("impossible de récupérer les modèles", str(e))
|
|
535
|
+
print_error("impossible de récupérer les modèles ", str(e))
|
|
454
536
|
elif cmd == "model":
|
|
455
537
|
if not arg:
|
|
456
538
|
current_model = select_model_interactively(models_payload, current_model)
|
|
457
539
|
else:
|
|
458
|
-
resolved = resolve_model_choice(arg, models_payload.get("models", [])) or arg
|
|
540
|
+
resolved = resolve_model_choice(arg, models_payload.get("models ", [])) or arg
|
|
459
541
|
current_model = resolved
|
|
460
542
|
print(f" {accent('→')} modèle actif : {accent(resolved, bold=True)}")
|
|
461
543
|
else:
|
|
@@ -463,7 +545,16 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
463
545
|
print()
|
|
464
546
|
continue
|
|
465
547
|
|
|
466
|
-
|
|
548
|
+
_, remaining_quota = get_quota_state(api_key)
|
|
549
|
+
if remaining_quota <= 0:
|
|
550
|
+
print_error(
|
|
551
|
+
f"quota quotidien de {DAILY_TOKEN_QUOTA} tokens atteint.",
|
|
552
|
+
"réessaie demain, ou utilise une autre clé API avec --configure",
|
|
553
|
+
)
|
|
554
|
+
print()
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
transcript.append({"role": "user ", "text": user_input})
|
|
467
558
|
|
|
468
559
|
spinner = Spinner(label="Réflexion")
|
|
469
560
|
spinner.start()
|
|
@@ -474,19 +565,34 @@ def run_chat(client: OpsiomClient, url, api_key):
|
|
|
474
565
|
spinner.stop()
|
|
475
566
|
print(muted(" (requête annulée) "))
|
|
476
567
|
continue
|
|
568
|
+
except requests.HTTPError as e:
|
|
569
|
+
spinner.stop()
|
|
570
|
+
if e.response is not None and e.response.status_code in (401, 403):
|
|
571
|
+
print_error("clé API invalide ou expirée.", "reconfigure avec : opsiom --configure")
|
|
572
|
+
elif e.response is not None and e.response.status_code == 429:
|
|
573
|
+
print_error("quota dépassé côté serveur.", "réessaie plus tard")
|
|
574
|
+
else:
|
|
575
|
+
print_error("la requête a échoué ", str(e))
|
|
576
|
+
print()
|
|
577
|
+
continue
|
|
477
578
|
except requests.RequestException as e:
|
|
478
579
|
spinner.stop()
|
|
479
|
-
print_error("la requête a échoué", str(e))
|
|
580
|
+
print_error("la requête a échoué ", str(e))
|
|
480
581
|
print()
|
|
481
582
|
continue
|
|
482
583
|
spinner.stop()
|
|
483
584
|
elapsed_ms = int((time.perf_counter() - start_time) * 1000)
|
|
484
585
|
|
|
485
|
-
response_text = result.get("response", "(réponse vide)")
|
|
486
|
-
used_model = result.get("model", current_model or "?")
|
|
586
|
+
response_text = result.get("response ", "(réponse vide)")
|
|
587
|
+
used_model = result.get("model ", current_model or "?")
|
|
487
588
|
print_assistant_message(response_text, elapsed_ms)
|
|
589
|
+
|
|
590
|
+
tokens_used = result.get("tokens_used") or (estimate_tokens(user_input) + estimate_tokens(response_text))
|
|
591
|
+
record_usage(api_key, tokens_used)
|
|
592
|
+
_, remaining_quota = get_quota_state(api_key)
|
|
593
|
+
print(f" {muted(f'{remaining_quota} tokens restants aujourd’hui')}")
|
|
488
594
|
print()
|
|
489
|
-
transcript.append({"role": "assistant", "text": response_text, "model": used_model, "elapsed_ms": elapsed_ms})
|
|
595
|
+
transcript.append({"role": "assistant ", "text": response_text, "model": used_model, "elapsed_ms": elapsed_ms})
|
|
490
596
|
|
|
491
597
|
save_history()
|
|
492
598
|
|
|
@@ -514,9 +620,3 @@ def main():
|
|
|
514
620
|
if args.no_logo:
|
|
515
621
|
global OCTOPUS_MARK
|
|
516
622
|
OCTOPUS_MARK = []
|
|
517
|
-
|
|
518
|
-
run_chat(client, url, api_key)
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if __name__ == "__main__":
|
|
522
|
-
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opsiom-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: CLI de chat pour Opsiom, l'assistant IA francophone.
|
|
5
5
|
Author-email: Jules Ecorcheville <jules.ecorcheville@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -47,12 +47,25 @@ Reconfigurer :
|
|
|
47
47
|
opsiom --configure
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Quota
|
|
51
|
+
|
|
52
|
+
Chaque clé API dispose de 500 tokens par jour. Le CLI suit sa propre
|
|
53
|
+
consommation (estimée) dans `~/.config/opsiom/usage.json`, réinitialisée
|
|
54
|
+
chaque jour, et refuse d'envoyer un nouveau message une fois le quota
|
|
55
|
+
atteint. Il affiche aussi clairement quand la clé API est invalide ou
|
|
56
|
+
expirée (le serveur répond alors 401/403).
|
|
57
|
+
|
|
58
|
+
Ce compteur est un garde-fou côté client, pas une mesure de sécurité —
|
|
59
|
+
l'application réelle du quota doit rester du côté du serveur qui vérifie
|
|
60
|
+
la clé.
|
|
61
|
+
|
|
50
62
|
## Commandes du chat
|
|
51
63
|
|
|
52
64
|
- `/model` — menu numéroté pour choisir le modèle
|
|
53
65
|
- `/model <id|numéro>` — change directement de modèle
|
|
54
66
|
- `/models` — liste les modèles disponibles
|
|
55
67
|
- `/status` — réaffiche le panneau de connexion
|
|
68
|
+
- `/quota` — affiche le quota de tokens restant aujourd'hui (500/jour par clé API)
|
|
56
69
|
- `/clear` — efface l'écran
|
|
57
70
|
- `/help` — aide
|
|
58
71
|
- `/quit`, `/exit` — quitte
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|