omni-ai-cli 1.1.0 → 1.3.0
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.
- package/README.md +4 -2
- package/docs/README.en.md +4 -2
- package/docs/README.es.md +4 -2
- package/docs/README.fr.md +4 -3
- package/omni_ai/chat.py +21 -3
- package/omni_ai/cli.py +38 -0
- package/omni_ai/openrouter.py +84 -6
- package/omni_ai/ui.py +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
8
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
9
|
[](https://docs.npmjs.com/trusted-publishers)
|
|
10
|
+
[](https://github.com/samuelrms/omni-ai-cli/actions/workflows/coverage.yml)
|
|
10
11
|
[](LICENSE)
|
|
11
12
|

|
|
12
13
|

|
|
@@ -388,6 +389,8 @@ ai --install # instala as ferramentas que faltam (perg
|
|
|
388
389
|
ai --classify "prompt" # mostra como seria classificado
|
|
389
390
|
ai --log # últimas chamadas
|
|
390
391
|
ai doctor # diagnóstico de instalação, contas e chaves
|
|
392
|
+
ai --version # versão do Omni AI CLI
|
|
393
|
+
ai --completions zsh|bash # autocomplete do shell
|
|
391
394
|
```
|
|
392
395
|
|
|
393
396
|
---
|
|
@@ -449,7 +452,7 @@ python3 -m unittest discover -s tests -v
|
|
|
449
452
|
bash -n scripts/install.sh
|
|
450
453
|
```
|
|
451
454
|
|
|
452
|
-
**
|
|
455
|
+
**123 testes**: persistência e retomada de sessão, limite de contexto, permissão dos
|
|
453
456
|
arquivos, sessão aberta em dois terminais, leitura de eventos, cancelamento da árvore de
|
|
454
457
|
processos, paralelismo real, sandbox somente leitura, mascaramento de dados pessoais,
|
|
455
458
|
fallback OpenRouter → Codex, validação de modelos, armazenamento das chaves e o ciclo de vida das contas. Todos os
|
|
@@ -543,7 +546,6 @@ guardar nem rotacionar.
|
|
|
543
546
|
arquivos. Os agentes não editam o projeto ao mesmo tempo.
|
|
544
547
|
- A classificação simples/complexa é por palavras-chave e tamanho, não por um modelo.
|
|
545
548
|
- Trocar de modelo não renova a cota da conta.
|
|
546
|
-
- O OpenRouter não tem streaming nesta versão: a resposta aparece inteira no final.
|
|
547
549
|
- No Windows, o código usa os equivalentes nativos (grupo de processos e `taskkill`, `msvcrt`, junções de pasta), mas foi testado de verdade só no macOS.
|
|
548
550
|
|
|
549
551
|
---
|
package/docs/README.en.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
8
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
9
|
[](https://docs.npmjs.com/trusted-publishers)
|
|
10
|
+
[](https://github.com/samuelrms/omni-ai-cli/actions/workflows/coverage.yml)
|
|
10
11
|
[](../LICENSE)
|
|
11
12
|

|
|
12
13
|

|
|
@@ -387,6 +388,8 @@ ai --install # installs the missing tools (asks first)
|
|
|
387
388
|
ai --classify "prompt" # shows how it would be classified
|
|
388
389
|
ai --log # latest calls
|
|
389
390
|
ai doctor # diagnose install, accounts and keys
|
|
391
|
+
ai --version # Omni AI CLI version
|
|
392
|
+
ai --completions zsh|bash # shell autocomplete
|
|
390
393
|
```
|
|
391
394
|
|
|
392
395
|
---
|
|
@@ -448,7 +451,7 @@ python3 -m unittest discover -s tests -v
|
|
|
448
451
|
bash -n scripts/install.sh
|
|
449
452
|
```
|
|
450
453
|
|
|
451
|
-
**
|
|
454
|
+
**123 tests**: session persistence and resume, context limit, file permissions, session
|
|
452
455
|
open in two terminals, event reading, process tree cancellation, real parallelism,
|
|
453
456
|
read-only sandbox, personal data masking, OpenRouter → Codex fallback, model
|
|
454
457
|
validation, key storage and the account lifecycle. All the
|
|
@@ -542,7 +545,6 @@ The workflow already requests `id-token: write` and `NPM_CONFIG_PROVENANCE`; the
|
|
|
542
545
|
access the files. The agents do not edit the project at the same time.
|
|
543
546
|
- The simple/complex classification is by keywords and length, not by a model.
|
|
544
547
|
- Switching models does not renew the account's quota.
|
|
545
|
-
- OpenRouter has no streaming in this version: the answer appears whole at the end.
|
|
546
548
|
- On Windows the code uses the native equivalents (process groups and `taskkill`, `msvcrt`, folder junctions), but it has only been tested for real on macOS.
|
|
547
549
|
|
|
548
550
|
---
|
package/docs/README.es.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
8
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
9
|
[](https://docs.npmjs.com/trusted-publishers)
|
|
10
|
+
[](https://github.com/samuelrms/omni-ai-cli/actions/workflows/coverage.yml)
|
|
10
11
|
[](../LICENSE)
|
|
11
12
|

|
|
12
13
|

|
|
@@ -388,6 +389,8 @@ ai --install # instala las herramientas que faltan (pr
|
|
|
388
389
|
ai --classify "prompt" # muestra cómo se clasificaría
|
|
389
390
|
ai --log # últimas llamadas
|
|
390
391
|
ai doctor # diagnóstico de instalación, cuentas y claves
|
|
392
|
+
ai --version # versión de Omni AI CLI
|
|
393
|
+
ai --completions zsh|bash # autocompletado del shell
|
|
391
394
|
```
|
|
392
395
|
|
|
393
396
|
---
|
|
@@ -449,7 +452,7 @@ python3 -m unittest discover -s tests -v
|
|
|
449
452
|
bash -n scripts/install.sh
|
|
450
453
|
```
|
|
451
454
|
|
|
452
|
-
**
|
|
455
|
+
**123 pruebas**: persistencia y reanudación de sesión, límite de contexto, permisos de
|
|
453
456
|
los archivos, sesión abierta en dos terminales, lectura de eventos, cancelación del árbol
|
|
454
457
|
de procesos, paralelismo real, sandbox de solo lectura, enmascaramiento de datos
|
|
455
458
|
personales, fallback OpenRouter → Codex, validación de modelos, almacenamiento de las
|
|
@@ -545,7 +548,6 @@ guardar ni rotar.
|
|
|
545
548
|
a los archivos. Los agentes no editan el proyecto al mismo tiempo.
|
|
546
549
|
- La clasificación simple/compleja se hace por palabras clave y tamaño, no con un modelo.
|
|
547
550
|
- Cambiar de modelo no renueva la cuota de la cuenta.
|
|
548
|
-
- OpenRouter no tiene streaming en esta versión: la respuesta aparece completa al final.
|
|
549
551
|
- En Windows el código usa los equivalentes nativos (grupos de procesos y `taskkill`, `msvcrt`, uniones de carpeta), pero solo se probó de verdad en macOS.
|
|
550
552
|
|
|
551
553
|
---
|
package/docs/README.fr.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
8
|
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
9
|
[](https://docs.npmjs.com/trusted-publishers)
|
|
10
|
+
[](https://github.com/samuelrms/omni-ai-cli/actions/workflows/coverage.yml)
|
|
10
11
|
[](../LICENSE)
|
|
11
12
|

|
|
12
13
|

|
|
@@ -397,6 +398,8 @@ ai --install # installe les outils manquants (demande
|
|
|
397
398
|
ai --classify "prompt" # montre la classification
|
|
398
399
|
ai --log # derniers appels
|
|
399
400
|
ai doctor # diagnostic installation, comptes et clés
|
|
401
|
+
ai --version # version d'Omni AI CLI
|
|
402
|
+
ai --completions zsh|bash # autocomplétion du shell
|
|
400
403
|
```
|
|
401
404
|
|
|
402
405
|
---
|
|
@@ -458,7 +461,7 @@ python3 -m unittest discover -s tests -v
|
|
|
458
461
|
bash -n scripts/install.sh
|
|
459
462
|
```
|
|
460
463
|
|
|
461
|
-
**
|
|
464
|
+
**123 tests** : persistance et reprise de session, limite de contexte, permissions des
|
|
462
465
|
fichiers, session ouverte dans deux terminaux, lecture des événements, annulation de
|
|
463
466
|
l'arbre de processus, parallélisme réel, sandbox en lecture seule, masquage des données
|
|
464
467
|
personnelles, fallback OpenRouter → Codex, validation des modèles, stockage des clés et
|
|
@@ -555,8 +558,6 @@ Le workflow demande déjà `id-token: write` et `NPM_CONFIG_PROVENANCE` ; il n'y
|
|
|
555
558
|
- La classification simple/complexe repose sur des mots-clés et la taille, pas sur un
|
|
556
559
|
modèle.
|
|
557
560
|
- Changer de modèle ne renouvelle pas le quota du compte.
|
|
558
|
-
- OpenRouter n'a pas de streaming dans cette version : la réponse apparaît en entier à
|
|
559
|
-
la fin.
|
|
560
561
|
- Sous Windows le code utilise les équivalents natifs (groupes de processus et `taskkill`, `msvcrt`, jonctions de dossier), mais il n'a été testé pour de vrai que sous macOS.
|
|
561
562
|
|
|
562
563
|
---
|
package/omni_ai/chat.py
CHANGED
|
@@ -154,6 +154,7 @@ T = {
|
|
|
154
154
|
/account [list|add|login|remove] [codex|agy|grok] NOME Contas (Claude por padrão; quantas quiser)
|
|
155
155
|
/key [add|remove] [{keys}] [NOME] Chaves de API (várias por ferramenta, usadas em ordem)
|
|
156
156
|
/status /reset Status das contas / zerar cooldowns
|
|
157
|
+
/doctor Diagnóstico de instalação, contas e chaves
|
|
157
158
|
/sync [--dry-run] Compartilhar MCPs e skills entre Claude, Codex e agy
|
|
158
159
|
/mode default|plan|edits|auto|yolo Modo de permissão (vale para Claude, Codex e agy)
|
|
159
160
|
/setup [install] Guia de primeiro acesso; install instala as ferramentas que faltam
|
|
@@ -177,6 +178,7 @@ T = {
|
|
|
177
178
|
/account [list|add|login|remove] [codex|agy|grok] NAME Accounts (Claude by default; as many as you want)
|
|
178
179
|
/key [add|remove] [{keys}] [NAME] API keys (several per tool, tried in order)
|
|
179
180
|
/status /reset Account status / clear cooldowns
|
|
181
|
+
/doctor Diagnose install, accounts and keys
|
|
180
182
|
/sync [--dry-run] Share MCPs and skills across Claude, Codex and agy
|
|
181
183
|
/mode default|plan|edits|auto|yolo Permission mode (applies to Claude, Codex and agy)
|
|
182
184
|
/setup [install] First-run guide; install installs the missing tools
|
|
@@ -200,6 +202,7 @@ T = {
|
|
|
200
202
|
/account [list|add|login|remove] [codex|agy|grok] NOMBRE Cuentas (Claude por defecto; las que quieras)
|
|
201
203
|
/key [add|remove] [{keys}] [NOMBRE] Claves de API (varias por herramienta, en orden)
|
|
202
204
|
/status /reset Estado de cuentas / limpiar cooldowns
|
|
205
|
+
/doctor Diagnóstico de instalación, cuentas y claves
|
|
203
206
|
/sync [--dry-run] Compartir MCPs y skills entre Claude, Codex y agy
|
|
204
207
|
/mode default|plan|edits|auto|yolo Modo de permisos (Claude, Codex y agy)
|
|
205
208
|
/setup [install] Guía de primer acceso; install instala las herramientas que faltan
|
|
@@ -223,6 +226,7 @@ T = {
|
|
|
223
226
|
/account [list|add|login|remove] [codex|agy|grok] NOM Comptes (Claude par défaut ; autant que voulu)
|
|
224
227
|
/key [add|remove] [{keys}] [NOM] Clés API (plusieurs par outil, essayées dans l'ordre)
|
|
225
228
|
/status /reset État des comptes / réinitialiser les cooldowns
|
|
229
|
+
/doctor Diagnostic installation, comptes et clés
|
|
226
230
|
/sync [--dry-run] Partager MCP et skills entre Claude, Codex et agy
|
|
227
231
|
/mode default|plan|edits|auto|yolo Mode de permissions (Claude, Codex et agy)
|
|
228
232
|
/setup [install] Guide de démarrage ; install installe les outils manquants
|
|
@@ -477,10 +481,24 @@ class Runner:
|
|
|
477
481
|
if job.get('dispatches'):
|
|
478
482
|
job['dispatches'][-1]['sent'] = event['data']
|
|
479
483
|
view.note('dados pessoais mascarados antes do envio · /sent para ver')
|
|
484
|
+
elif kind == 'smart.delta':
|
|
485
|
+
# Stream the answer line by line as the tokens arrive (OpenRouter).
|
|
486
|
+
job['stream_buf'] = job.get('stream_buf', '') + event.get('text', '')
|
|
487
|
+
job['streamed'] = True
|
|
488
|
+
while '\n' in job['stream_buf']:
|
|
489
|
+
line, job['stream_buf'] = job['stream_buf'].split('\n', 1)
|
|
490
|
+
view.stream_line(line)
|
|
480
491
|
elif kind == 'smart.answer':
|
|
481
492
|
job['model'] = event.get('model')
|
|
482
493
|
spend(job, event.get('usage') or {})
|
|
483
|
-
|
|
494
|
+
if job.get('streamed'):
|
|
495
|
+
if job.get('stream_buf'):
|
|
496
|
+
view.stream_line(job['stream_buf'])
|
|
497
|
+
job['stream_buf'] = ''
|
|
498
|
+
# the streamed lines were display-only; keep the full, unmasked answer for the session
|
|
499
|
+
job['answers'] = [event.get('content', '')]
|
|
500
|
+
else:
|
|
501
|
+
self.answer(job, event.get('content', ''))
|
|
484
502
|
view.note('modelo: ' + str(event.get('model', '?')))
|
|
485
503
|
elif kind in ('item.started', 'item.completed'):
|
|
486
504
|
item = event.get('item', {})
|
|
@@ -1016,7 +1034,7 @@ def compact(session):
|
|
|
1016
1034
|
|
|
1017
1035
|
|
|
1018
1036
|
TOOL_COMMANDS = {'/usage': ['--usage'], '/limits': ['--usage'], '/model': ['--model'], '/account': ['--account'], '/key': ['--key'],
|
|
1019
|
-
'/status': ['--status'], '/sync': ['--sync'], '/update': ['--update'], '/reset': ['--reset']}
|
|
1037
|
+
'/status': ['--status'], '/doctor': ['--doctor'], '/sync': ['--sync'], '/update': ['--update'], '/reset': ['--reset']}
|
|
1020
1038
|
|
|
1021
1039
|
|
|
1022
1040
|
def run_tool(args):
|
|
@@ -1027,7 +1045,7 @@ def run_tool(args):
|
|
|
1027
1045
|
pass
|
|
1028
1046
|
|
|
1029
1047
|
|
|
1030
|
-
BUILTIN = ['/usage', '/clear', '/compact', '/queue', '/versions', '/setup', '/terminal-setup', '/tool', '/model', '/account', '/key', '/status', '/reset', '/sync', '/update', '/mode', '/lang',
|
|
1048
|
+
BUILTIN = ['/usage', '/clear', '/compact', '/queue', '/versions', '/setup', '/terminal-setup', '/tool', '/model', '/account', '/key', '/status', '/doctor', '/reset', '/sync', '/update', '/mode', '/lang',
|
|
1031
1049
|
'/skills', '/history', '/sent', '/trace', '/sessions', '/resume', '/new', '/parallel', '/help', '/exit']
|
|
1032
1050
|
|
|
1033
1051
|
|
package/omni_ai/cli.py
CHANGED
|
@@ -690,6 +690,37 @@ def ai_version():
|
|
|
690
690
|
return '?'
|
|
691
691
|
|
|
692
692
|
|
|
693
|
+
def completion_words():
|
|
694
|
+
"""Every top-level word `ai` accepts, for shell completion."""
|
|
695
|
+
flags = ['--model', '--account', '--key', '--keys', '--sync', '--usage', '--limits', '--setup',
|
|
696
|
+
'--install', '--status', '--doctor', '--reset', '--log', '--classify', '--update',
|
|
697
|
+
'--mode', '--chat', '--resume', '--sessions', '--new', '--help', '--version', '--completions']
|
|
698
|
+
return flags + ['--' + name for name in TOOLS] + ['doctor']
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def completions(shell):
|
|
702
|
+
"""A completion script for `ai`, printed for the user to source. Completes the top-level flags."""
|
|
703
|
+
words = ' '.join(completion_words())
|
|
704
|
+
if shell == 'bash':
|
|
705
|
+
say(f'''# Omni AI CLI bash completion. Install: ai --completions bash > ~/.ai-completion.bash
|
|
706
|
+
# then add to ~/.bashrc: source ~/.ai-completion.bash
|
|
707
|
+
_ai_complete() {{
|
|
708
|
+
local cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
709
|
+
COMPREPLY=( $(compgen -W "{words}" -- "$cur") )
|
|
710
|
+
}}
|
|
711
|
+
complete -F _ai_complete ai''')
|
|
712
|
+
return 0
|
|
713
|
+
if shell == 'zsh':
|
|
714
|
+
say(f'''#compdef ai
|
|
715
|
+
# Omni AI CLI zsh completion. Install: ai --completions zsh > ~/.zsh/_ai
|
|
716
|
+
# then ensure that dir is on $fpath before compinit in ~/.zshrc
|
|
717
|
+
_ai() {{ compadd -- {words} }}
|
|
718
|
+
compdef _ai ai''')
|
|
719
|
+
return 0
|
|
720
|
+
say(f'{RED}✗ Shell não suportado: {shell}. Use zsh ou bash.{NC}')
|
|
721
|
+
return 1
|
|
722
|
+
|
|
723
|
+
|
|
693
724
|
def doctor():
|
|
694
725
|
"""Health check: what is installed, logged in and configured, and what is missing. Returns 1 when
|
|
695
726
|
nothing can answer a request (no Claude login and no API key), 0 otherwise."""
|
|
@@ -987,6 +1018,8 @@ USAGE = f'''{BOLD}Uso:{NC}
|
|
|
987
1018
|
ai --install Instala as ferramentas que faltam (pergunta antes)
|
|
988
1019
|
ai --status | --doctor | --reset | --log | --classify "prompt"
|
|
989
1020
|
ai --update Atualizar para a versão mais nova
|
|
1021
|
+
ai --version Versão do Omni AI CLI
|
|
1022
|
+
ai --completions zsh|bash Script de autocomplete do shell
|
|
990
1023
|
'''
|
|
991
1024
|
|
|
992
1025
|
|
|
@@ -1037,6 +1070,11 @@ def main(argv):
|
|
|
1037
1070
|
return 0
|
|
1038
1071
|
elif arg in ('--doctor', 'doctor'):
|
|
1039
1072
|
return doctor()
|
|
1073
|
+
elif arg in ('--version', '-v'):
|
|
1074
|
+
say(ai_version())
|
|
1075
|
+
return 0
|
|
1076
|
+
elif arg == '--completions':
|
|
1077
|
+
return completions(argv[i + 1] if i + 1 < len(argv) else '')
|
|
1040
1078
|
elif arg in ('--setup', '--install'):
|
|
1041
1079
|
from . import onboarding
|
|
1042
1080
|
onboarding.main(install_missing=arg == '--install' or 'install' in argv[i + 1:])
|
package/omni_ai/openrouter.py
CHANGED
|
@@ -105,6 +105,55 @@ class OpenRouterClient:
|
|
|
105
105
|
except urllib.error.URLError as e:
|
|
106
106
|
raise RuntimeError("Falha de rede ao acessar OpenRouter") from None
|
|
107
107
|
|
|
108
|
+
def _stream_request(self, data: dict):
|
|
109
|
+
"""POST /chat/completions with stream=True, yielding ('delta', text) per token and a final
|
|
110
|
+
('final', {content, model, usage}). Raises RuntimeError on any failure, like _request."""
|
|
111
|
+
if os.environ.get('SMART_AI_EVENTS') == '1':
|
|
112
|
+
print(json.dumps({'type': 'smart.request', 'data': data}, ensure_ascii=False), file=sys.stderr, flush=True)
|
|
113
|
+
headers = {
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"HTTP-Referer": "https://github.com/samuelrms/omni-ai-cli",
|
|
116
|
+
"X-Title": "Omni AI CLI",
|
|
117
|
+
"Accept": "text/event-stream",
|
|
118
|
+
}
|
|
119
|
+
if self.api_key:
|
|
120
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
121
|
+
body = json.dumps(dict(data, stream=True, stream_options={"include_usage": True})).encode()
|
|
122
|
+
req = urllib.request.Request(f"{OPENROUTER_BASE}/chat/completions", data=body, headers=headers, method="POST")
|
|
123
|
+
content, model, usage = [], None, {}
|
|
124
|
+
try:
|
|
125
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
126
|
+
for raw in resp:
|
|
127
|
+
line = raw.decode("utf-8", "replace").strip()
|
|
128
|
+
if not line.startswith("data:"):
|
|
129
|
+
continue
|
|
130
|
+
payload = line[5:].strip()
|
|
131
|
+
if payload == "[DONE]":
|
|
132
|
+
break
|
|
133
|
+
try:
|
|
134
|
+
chunk = json.loads(payload)
|
|
135
|
+
except ValueError:
|
|
136
|
+
continue
|
|
137
|
+
model = chunk.get("model") or model
|
|
138
|
+
usage = chunk.get("usage") or usage
|
|
139
|
+
for choice in chunk.get("choices", []):
|
|
140
|
+
piece = (choice.get("delta") or {}).get("content") or ""
|
|
141
|
+
if piece:
|
|
142
|
+
content.append(piece)
|
|
143
|
+
yield ("delta", piece)
|
|
144
|
+
except urllib.error.HTTPError as e:
|
|
145
|
+
try:
|
|
146
|
+
reason = json.loads(e.read().decode())["error"]["message"][:160]
|
|
147
|
+
except (ValueError, KeyError, TypeError, OSError):
|
|
148
|
+
reason = ""
|
|
149
|
+
e.close()
|
|
150
|
+
raise RuntimeError(f"OpenRouter HTTP {e.code}" + (f": {reason}" if reason else "")) from None
|
|
151
|
+
except (ValueError, TimeoutError):
|
|
152
|
+
raise RuntimeError("Resposta inválida ou timeout do OpenRouter") from None
|
|
153
|
+
except urllib.error.URLError:
|
|
154
|
+
raise RuntimeError("Falha de rede ao acessar OpenRouter") from None
|
|
155
|
+
yield ("final", {"content": "".join(content), "model": model, "usage": usage})
|
|
156
|
+
|
|
108
157
|
def list_free_models(self) -> list:
|
|
109
158
|
"""Lista todos os modelos gratuitos disponíveis."""
|
|
110
159
|
resp = self._request("/models")
|
|
@@ -122,6 +171,7 @@ class OpenRouterClient:
|
|
|
122
171
|
system_prompt: str = None,
|
|
123
172
|
temperature: float = 0.7,
|
|
124
173
|
max_tokens: int = 4096,
|
|
174
|
+
on_delta=None,
|
|
125
175
|
) -> dict:
|
|
126
176
|
"""
|
|
127
177
|
Envia um prompt e retorna a resposta.
|
|
@@ -185,10 +235,26 @@ class OpenRouterClient:
|
|
|
185
235
|
if not self.allow_paid:
|
|
186
236
|
data["provider"] = {"max_price": {"prompt": 0, "completion": 0}}
|
|
187
237
|
try:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
238
|
+
if on_delta is not None:
|
|
239
|
+
# Stream the tokens as they arrive; a chunk may still be a masked placeholder, so
|
|
240
|
+
# unmask each piece best-effort and unmask the whole answer again at the end.
|
|
241
|
+
final = None
|
|
242
|
+
for kind, payload in self._stream_request(data):
|
|
243
|
+
if kind == "delta":
|
|
244
|
+
piece = self.masker.unmask(payload, self._pii_map) if (self.masker and self._pii_map) else payload
|
|
245
|
+
on_delta(piece)
|
|
246
|
+
else:
|
|
247
|
+
final = payload
|
|
248
|
+
if final and final["content"].strip():
|
|
249
|
+
resp = {"choices": [{"message": {"content": final["content"]}}],
|
|
250
|
+
"model": final["model"], "usage": final["usage"]}
|
|
251
|
+
break
|
|
252
|
+
last_error = RuntimeError("OpenRouter não retornou uma resposta válida")
|
|
253
|
+
else:
|
|
254
|
+
resp = self._request("/chat/completions", data=data, method="POST")
|
|
255
|
+
if resp.get("choices"):
|
|
256
|
+
break
|
|
257
|
+
last_error = RuntimeError("OpenRouter não retornou uma resposta válida")
|
|
192
258
|
except RuntimeError as error:
|
|
193
259
|
last_error = error
|
|
194
260
|
# Bad key or no credit: another model won't help
|
|
@@ -310,19 +376,31 @@ def main():
|
|
|
310
376
|
parser.print_help()
|
|
311
377
|
sys.exit(1)
|
|
312
378
|
|
|
379
|
+
events = os.environ.get('SMART_AI_EVENTS') == '1'
|
|
380
|
+
# Stream tokens for the chat (events) and for a human at a terminal; keep --json buffered.
|
|
381
|
+
if events:
|
|
382
|
+
on_delta = lambda piece: print(json.dumps({'type': 'smart.delta', 'text': piece}, ensure_ascii=False), flush=True)
|
|
383
|
+
elif not args.json:
|
|
384
|
+
on_delta = lambda piece: (sys.stdout.write(piece), sys.stdout.flush())
|
|
385
|
+
else:
|
|
386
|
+
on_delta = None
|
|
387
|
+
|
|
313
388
|
try:
|
|
314
389
|
result = client.chat(
|
|
315
390
|
prompt=prompt,
|
|
316
391
|
system_prompt=args.system,
|
|
317
392
|
temperature=args.temperature,
|
|
393
|
+
on_delta=on_delta,
|
|
318
394
|
)
|
|
319
395
|
|
|
320
|
-
if
|
|
396
|
+
if events:
|
|
321
397
|
print(json.dumps({'type': 'smart.answer', **result}, ensure_ascii=False), flush=True)
|
|
322
398
|
elif args.json:
|
|
323
399
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
324
|
-
|
|
400
|
+
elif on_delta is None:
|
|
325
401
|
print(result["content"])
|
|
402
|
+
else:
|
|
403
|
+
print() # end the streamed line
|
|
326
404
|
|
|
327
405
|
except RuntimeError as e:
|
|
328
406
|
print(f"❌ Erro: {e}", file=sys.stderr)
|
package/omni_ai/ui.py
CHANGED
|
@@ -214,10 +214,20 @@ class View:
|
|
|
214
214
|
def __init__(self, prefix=''):
|
|
215
215
|
self.prefix = prefix
|
|
216
216
|
self.pending = None # last action, so its result can be attached under it
|
|
217
|
+
self._streamed = False # whether a streaming answer has started printing
|
|
217
218
|
|
|
218
219
|
def _p(self, text):
|
|
219
220
|
out('\n'.join(self.prefix + line for line in text.split('\n')))
|
|
220
221
|
|
|
222
|
+
def stream_line(self, line):
|
|
223
|
+
"""One completed line of a streaming answer, printed as it arrives (line-granular keeps the
|
|
224
|
+
chat's input box intact, unlike raw partial writes). The first line carries the answer mark."""
|
|
225
|
+
if not self._streamed:
|
|
226
|
+
self._p('\n' + gradient(MARK) + ' ' + line)
|
|
227
|
+
self._streamed = True
|
|
228
|
+
else:
|
|
229
|
+
self._p(' ' + line)
|
|
230
|
+
|
|
221
231
|
def provider(self, tool, detail):
|
|
222
232
|
self._p(dim(' ') + rgb(TOOL_RGB.get(tool, (6, 182, 212)), '● ' + tool) + (dim(' · ' + detail) if detail else ''))
|
|
223
233
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omni-ai-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "One chat for Claude Code, Codex, Antigravity and OpenRouter, with automatic routing, account fallback, API keys, permission modes and MCP/skill sync.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai": "bin/ai.js",
|