@neetru/cli 2.32.1 → 2.34.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/CHANGELOG.md +78 -0
- package/README.md +38 -3
- package/dist/commands/admin/sdk-credential.js +3 -1
- package/dist/commands/ai-models.d.ts +2 -1
- package/dist/commands/ai-models.js +4 -5
- package/dist/commands/ai.d.ts +12 -2
- package/dist/commands/ai.js +47 -38
- package/dist/commands/auth.d.ts +3 -2
- package/dist/commands/auth.js +96 -57
- package/dist/commands/build.js +35 -4
- package/dist/commands/chat.d.ts +24 -6
- package/dist/commands/chat.js +43 -7
- package/dist/commands/code.js +11 -2
- package/dist/commands/config.js +15 -13
- package/dist/commands/db-frontdoor.d.ts +116 -0
- package/dist/commands/db-frontdoor.js +572 -0
- package/dist/commands/db-move.d.ts +5 -0
- package/dist/commands/db-move.js +27 -0
- package/dist/commands/db.d.ts +8 -2
- package/dist/commands/db.js +13 -6
- package/dist/commands/deploy.d.ts +14 -5
- package/dist/commands/deploy.js +376 -55
- package/dist/commands/dev-context.js +1 -1
- package/dist/commands/environments-repoint.d.ts +49 -0
- package/dist/commands/environments-repoint.js +219 -0
- package/dist/commands/init.js +7 -1
- package/dist/commands/new.js +14 -0
- package/dist/commands/secrets.d.ts +6 -4
- package/dist/commands/secrets.js +14 -7
- package/dist/commands/servers.d.ts +29 -0
- package/dist/commands/servers.js +108 -1
- package/dist/commands/ui.js +1 -1
- package/dist/commands/workspaces.js +16 -1
- package/dist/index.js +103 -7
- package/dist/lib/ai/auth-discovery.d.ts +9 -12
- package/dist/lib/ai/auth-discovery.js +43 -145
- package/dist/lib/ai/orchestrator.d.ts +13 -6
- package/dist/lib/ai/orchestrator.js +46 -246
- package/dist/lib/ai/provedores.d.ts +57 -0
- package/dist/lib/ai/provedores.js +66 -0
- package/dist/lib/api-client.d.ts +8 -1
- package/dist/lib/api-client.js +18 -2
- package/dist/lib/build-targets.d.ts +48 -10
- package/dist/lib/build-targets.js +64 -10
- package/dist/lib/cli-write.js +6 -3
- package/dist/lib/code-agent-gateway.d.ts +2 -2
- package/dist/lib/code-agent-gateway.js +2 -2
- package/dist/lib/code-agent-settings.d.ts +2 -2
- package/dist/lib/code-agent-settings.js +2 -4
- package/dist/lib/code-agent-types.d.ts +3 -1
- package/dist/lib/config.d.ts +4 -7
- package/dist/lib/config.js +9 -6
- package/dist/lib/deploy-recusa-de-destino.d.ts +9 -0
- package/dist/lib/deploy-recusa-de-destino.js +98 -0
- package/dist/lib/deploy-render-probe.d.ts +117 -0
- package/dist/lib/deploy-render-probe.js +475 -0
- package/dist/lib/deploy-vm-um-app.d.ts +62 -0
- package/dist/lib/deploy-vm-um-app.js +83 -0
- package/dist/lib/segredo-produto.d.ts +25 -5
- package/dist/lib/segredo-produto.js +45 -10
- package/dist/lib/sse.d.ts +4 -5
- package/dist/lib/sse.js +4 -5
- package/package.json +1 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
2
1
|
import OpenAI from 'openai';
|
|
3
2
|
import { config } from '../config.js';
|
|
4
3
|
import { NEETRU_SYSTEM_CONTEXT, loadProjectContext } from './context.js';
|
|
@@ -7,6 +6,7 @@ import { getActiveTokenSync } from '../token-resolver.js';
|
|
|
7
6
|
import { maybeWarnUntrustedHost } from '../api-client.js';
|
|
8
7
|
import { readSseLines } from '../sse.js';
|
|
9
8
|
import { log } from '../../utils/logger.js';
|
|
9
|
+
import { CHAVE_DO_PROVEDOR, MODELOS_DO_AI_NA_TELA, NOMES_DOS_PROVEDORES, PROVEDORES_IA, ehModeloDoAi, } from './provedores.js';
|
|
10
10
|
// ── Endpoints + modelos default dos providers OpenAI-compatible ──────────────
|
|
11
11
|
// Groq e NVIDIA NIM expõem APIs OpenAI-compatible → reusamos o SDK `openai`
|
|
12
12
|
// só trocando `baseURL`. Modelos são overridáveis via `neetru config set
|
|
@@ -19,12 +19,7 @@ const NVIDIA_DEFAULT_MODEL = 'meta/llama-3.3-70b-instruct';
|
|
|
19
19
|
// OpenAI: gpt-4.1 (lançado 2025-04; mantém razão custo/capacidade melhor que
|
|
20
20
|
// gpt-4o para tasks de código e análise; overridável via `neetru config set
|
|
21
21
|
// openaiModel <id>`).
|
|
22
|
-
// Gemini: gemini-2.5-flash (lançado 2025-05; substitui 1.5-flash com thinking
|
|
23
|
-
// mode e contexto 1M; overridável via `neetru config set geminiModel <id>`).
|
|
24
|
-
// Claude: claude-sonnet-4-6 (já era; mantido).
|
|
25
22
|
const OPENAI_DEFAULT_MODEL = 'gpt-4.1';
|
|
26
|
-
const GEMINI_DEFAULT_MODEL = 'gemini-2.5-flash';
|
|
27
|
-
const CLAUDE_DEFAULT_MODEL = 'claude-sonnet-4-6';
|
|
28
23
|
// ── AI Gateway do Core (2026-07) ─────────────────────────────────────────────
|
|
29
24
|
// `neetru ai --debug:model gateway` — usa o token de sessão `nrt_...` gravado
|
|
30
25
|
// por `neetru login` (RFC 8628 device code) em vez de uma chave BYOK própria.
|
|
@@ -66,18 +61,7 @@ export function resetSessionBudget() {
|
|
|
66
61
|
}
|
|
67
62
|
/** Mapeia provider → chave no config local do Neetru. */
|
|
68
63
|
function configKeyFor(provider) {
|
|
69
|
-
|
|
70
|
-
case 'anthropic':
|
|
71
|
-
return 'anthropicApiKey';
|
|
72
|
-
case 'openai':
|
|
73
|
-
return 'openaiApiKey';
|
|
74
|
-
case 'gemini':
|
|
75
|
-
return 'geminiApiKey';
|
|
76
|
-
case 'groq':
|
|
77
|
-
return 'groqApiKey';
|
|
78
|
-
case 'nvidia':
|
|
79
|
-
return 'nvidiaApiKey';
|
|
80
|
-
}
|
|
64
|
+
return CHAVE_DO_PROVEDOR[provider];
|
|
81
65
|
}
|
|
82
66
|
/**
|
|
83
67
|
* Parseia um valor de chave que PODE conter várias chaves separadas por
|
|
@@ -128,134 +112,65 @@ function nextPoolStart(provider, len) {
|
|
|
128
112
|
*
|
|
129
113
|
* Heurísticas (em ordem):
|
|
130
114
|
* 1. Se user forçou um modelo (!= auto), respeita — desde que tenha cred.
|
|
131
|
-
* 2. Code generation pesado →
|
|
132
|
-
* 3. Sumarização rápida / Q&A factual → Groq (mais rápido)
|
|
133
|
-
* 4.
|
|
134
|
-
*
|
|
115
|
+
* 2. Code generation pesado → NVIDIA NIM (llama-70b, gratuito).
|
|
116
|
+
* 3. Sumarização rápida / Q&A factual → Groq (mais rápido).
|
|
117
|
+
* 4. Fallback: primeiro provider com cred, gratuitos primeiro
|
|
118
|
+
* (groq > nvidia > openai).
|
|
119
|
+
*
|
|
120
|
+
* bug_19d312f3: o default era Claude (API da Anthropic) e o Q&A rápido caía no
|
|
121
|
+
* Gemini quando faltava o Groq. Os dois saíram da lista única (vetados).
|
|
122
|
+
* Revisão do #104: o `auto` preferia a OpenAI (paga) na geração de código e no
|
|
123
|
+
* fallback, à frente do Groq e da NVIDIA. Agora ela só entra no `auto` quando
|
|
124
|
+
* nenhum provedor gratuito tem chave; quem a quer antes pede com `-m openai`.
|
|
135
125
|
*/
|
|
136
126
|
export function routeModel(input, preferred) {
|
|
137
127
|
const available = {
|
|
138
|
-
claude: !!resolveKey('anthropic'),
|
|
139
|
-
openai: !!resolveKey('openai'),
|
|
140
|
-
gemini: !!resolveKey('gemini'),
|
|
141
|
-
groq: !!resolveKey('groq'),
|
|
142
|
-
nvidia: !!resolveKey('nvidia'),
|
|
143
128
|
// Gateway não é BYOK — "disponível" = tem sessão de `neetru login` válida
|
|
144
129
|
// (mesmo resolver usado por todo o resto do CLI), não uma chave de pool.
|
|
145
130
|
gateway: !!getActiveTokenSync(),
|
|
146
131
|
};
|
|
132
|
+
for (const p of PROVEDORES_IA)
|
|
133
|
+
available[p] = !!resolveKey(p);
|
|
147
134
|
if (preferred !== 'auto') {
|
|
148
135
|
if (available[preferred])
|
|
149
136
|
return preferred;
|
|
150
137
|
// Pediu mas não tem cred — cai pra auto.
|
|
151
138
|
}
|
|
152
139
|
const lower = input.toLowerCase();
|
|
153
|
-
// Code generation pesado →
|
|
140
|
+
// Code generation pesado → NVIDIA NIM (llama grande, gratuito). Sem ela, segue
|
|
141
|
+
// a ordem do fallback (gratuitos primeiro).
|
|
154
142
|
const isCodeGen = /\b(crie|gere|escreva|implemente|escreve|gera|cria)\s+(um|uma|o|a|essa|esse|esta|este)?\s*(componente|função|funcao|classe|módulo|modulo|arquivo|test|teste|endpoint|rota|hook|serviço|servico|api)\b/i.test(lower) || /^```|^class\s|^function\s|^const\s+\w+\s*=\s*\(/m.test(input);
|
|
155
143
|
if (isCodeGen) {
|
|
156
|
-
if (available.openai)
|
|
157
|
-
return 'openai';
|
|
158
144
|
if (available.nvidia)
|
|
159
145
|
return 'nvidia';
|
|
160
146
|
}
|
|
161
|
-
// Sumarização rápida / Q&A factual → Groq (latência mais baixa)
|
|
147
|
+
// Sumarização rápida / Q&A factual → Groq (latência mais baixa).
|
|
162
148
|
const isQuickQa = /\b(o que é|o que e|defina|defina o|defina a|resuma|resume|sumariza|sumarize|que faz|para que serve)\b/i.test(lower) && input.length < 240;
|
|
163
149
|
if (isQuickQa) {
|
|
164
150
|
if (available.groq)
|
|
165
151
|
return 'groq';
|
|
166
|
-
if (available.gemini)
|
|
167
|
-
return 'gemini';
|
|
168
152
|
}
|
|
169
|
-
//
|
|
170
|
-
if (available.claude)
|
|
171
|
-
return 'claude';
|
|
172
|
-
if (available.openai)
|
|
173
|
-
return 'openai';
|
|
174
|
-
if (available.gemini)
|
|
175
|
-
return 'gemini';
|
|
153
|
+
// Gratuitos primeiro; a OpenAI (paga) só sem nenhum deles.
|
|
176
154
|
if (available.groq)
|
|
177
155
|
return 'groq';
|
|
178
156
|
if (available.nvidia)
|
|
179
157
|
return 'nvidia';
|
|
158
|
+
if (available.openai)
|
|
159
|
+
return 'openai';
|
|
160
|
+
// Sem chave de provedor nenhum. O `auto` não cai no AI Gateway por conta
|
|
161
|
+
// própria (gasta a cota do gateway), mas quem tem sessão do `neetru login`
|
|
162
|
+
// fica sabendo como pedi-lo — sem isto, cada pergunta só dizia "rode neetru
|
|
163
|
+
// auth setup" (revisão do #104).
|
|
164
|
+
if (available.gateway) {
|
|
165
|
+
throw new Error(`Nenhum provedor de IA configurado com chave (${NOMES_DOS_PROVEDORES}). ` +
|
|
166
|
+
'Para usar o AI Gateway da Neetru com a sua sessão do `neetru login`, abra com `neetru ai -m gateway` ' +
|
|
167
|
+
'(nesta sessão: /debug:model gateway) ou grave `neetru config set defaultModel gateway`. ' +
|
|
168
|
+
'Para usar uma chave sua: neetru auth setup');
|
|
169
|
+
}
|
|
180
170
|
// Sem creds em lugar nenhum — joga erro descritivo. Caller pode oferecer
|
|
181
171
|
// `neetru auth setup` antes de tentar.
|
|
182
172
|
throw new Error('Nenhum provedor de IA configurado. Rode: neetru auth setup');
|
|
183
173
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Streamer Claude/Anthropic com pool de chaves, timeout e usage tracking.
|
|
186
|
-
* Faz rotação round-robin igual ao streamOpenAICompatible: começa numa chave
|
|
187
|
-
* do pool e, em 401/403/429, cai pra próxima (fallthrough). Timeout via
|
|
188
|
-
* AbortController (STREAM_TIMEOUT_MS). Usage capturado do evento final_message.
|
|
189
|
-
*/
|
|
190
|
-
async function streamClaude(messages, systemExtra, onChunk) {
|
|
191
|
-
const keys = resolveKeyPool('anthropic');
|
|
192
|
-
if (keys.length === 0) {
|
|
193
|
-
throw new Error('Cred Claude/Anthropic ausente. Rode `neetru auth setup` ou `neetru config set anthropicApiKey <sk-ant-...>`');
|
|
194
|
-
}
|
|
195
|
-
const system = [NEETRU_SYSTEM_CONTEXT, systemExtra].filter(Boolean).join('\n\n');
|
|
196
|
-
const model = config.get('claudeModel') ?? CLAUDE_DEFAULT_MODEL;
|
|
197
|
-
const start = nextPoolStart('anthropic', keys.length);
|
|
198
|
-
let lastErr;
|
|
199
|
-
for (let i = 0; i < keys.length; i++) {
|
|
200
|
-
const apiKey = keys[(start + i) % keys.length];
|
|
201
|
-
const ctrl = new AbortController();
|
|
202
|
-
const timer = setTimeout(() => ctrl.abort(new Error(`Claude stream timeout (${STREAM_TIMEOUT_MS}ms)`)), STREAM_TIMEOUT_MS);
|
|
203
|
-
try {
|
|
204
|
-
const client = new Anthropic({ apiKey });
|
|
205
|
-
let full = '';
|
|
206
|
-
let usage;
|
|
207
|
-
const stream = await client.messages.stream({
|
|
208
|
-
model,
|
|
209
|
-
max_tokens: 8096,
|
|
210
|
-
system,
|
|
211
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
212
|
-
}, { signal: ctrl.signal });
|
|
213
|
-
for await (const chunk of stream) {
|
|
214
|
-
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
|
|
215
|
-
onChunk(chunk.delta.text);
|
|
216
|
-
full += chunk.delta.text;
|
|
217
|
-
}
|
|
218
|
-
if (chunk.type === 'message_delta' && chunk.usage) {
|
|
219
|
-
// usage aparece no evento message_delta do streaming
|
|
220
|
-
const u = chunk.usage;
|
|
221
|
-
usage = {
|
|
222
|
-
inputTokens: u.input_tokens ?? 0,
|
|
223
|
-
outputTokens: u.output_tokens ?? 0,
|
|
224
|
-
totalTokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// Captura usage do objeto final da stream se não veio via eventos.
|
|
229
|
-
if (!usage) {
|
|
230
|
-
const finalMsg = await stream.finalMessage().catch(() => null);
|
|
231
|
-
if (finalMsg?.usage) {
|
|
232
|
-
usage = {
|
|
233
|
-
inputTokens: finalMsg.usage.input_tokens ?? 0,
|
|
234
|
-
outputTokens: finalMsg.usage.output_tokens ?? 0,
|
|
235
|
-
totalTokens: (finalMsg.usage.input_tokens ?? 0) + (finalMsg.usage.output_tokens ?? 0),
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
recordUsage(usage);
|
|
240
|
-
return full;
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
lastErr = err;
|
|
244
|
-
// Timeout → propaga direto (não adianta tentar outra chave).
|
|
245
|
-
if (ctrl.signal.aborted)
|
|
246
|
-
throw err;
|
|
247
|
-
const status = err?.status ?? err?.response?.status;
|
|
248
|
-
if (status === 401 || status === 403 || status === 429)
|
|
249
|
-
continue;
|
|
250
|
-
throw err;
|
|
251
|
-
}
|
|
252
|
-
finally {
|
|
253
|
-
clearTimeout(timer);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const detail = lastErr?.message ?? String(lastErr);
|
|
257
|
-
throw new Error(`Claude: todas as ${keys.length} chave(s) do pool falharam (auth/rate-limit). Último erro: ${detail}`);
|
|
258
|
-
}
|
|
259
174
|
/**
|
|
260
175
|
* Streamer genérico pra qualquer endpoint OpenAI-compatible (OpenAI, Groq,
|
|
261
176
|
* NVIDIA NIM). Faz rotação de pool: começa numa chave round-robin e, em
|
|
@@ -369,117 +284,6 @@ function streamNvidia(messages, systemExtra, onChunk) {
|
|
|
369
284
|
onChunk,
|
|
370
285
|
});
|
|
371
286
|
}
|
|
372
|
-
/**
|
|
373
|
-
* Stream via Gemini REST API `streamGenerateContent`.
|
|
374
|
-
*
|
|
375
|
-
* Melhorias 2026-06-08:
|
|
376
|
-
* - Pool de chaves (comma-separated em geminiApiKey): rotação round-robin +
|
|
377
|
-
* fallthrough em 401/403/429 (igual ao OpenAI-compatible).
|
|
378
|
-
* - Modelo default atualizado: gemini-2.5-flash (substitui gemini-1.5-flash).
|
|
379
|
-
* Override: neetru config set geminiModel <id>
|
|
380
|
-
* - Timeout via AbortController (STREAM_TIMEOUT_MS).
|
|
381
|
-
* - Usage tracking: captura usageMetadata do último objeto SSE.
|
|
382
|
-
* - Linka signal externo (parâmetro) com o AbortController interno.
|
|
383
|
-
* - Cleanup: reader.cancel() em finally.
|
|
384
|
-
*/
|
|
385
|
-
async function streamGemini(messages, systemExtra, onChunk, signal) {
|
|
386
|
-
const keys = resolveKeyPool('gemini');
|
|
387
|
-
if (keys.length === 0) {
|
|
388
|
-
throw new Error('Cred Gemini ausente. Rode `neetru auth setup` ou `neetru config set geminiApiKey <key>`');
|
|
389
|
-
}
|
|
390
|
-
const systemContent = [NEETRU_SYSTEM_CONTEXT, systemExtra].filter(Boolean).join('\n\n');
|
|
391
|
-
const model = config.get('geminiModel') ?? GEMINI_DEFAULT_MODEL;
|
|
392
|
-
// Gemini API expects user/model roles (não assistant). Converte.
|
|
393
|
-
const contents = messages.map((m) => ({
|
|
394
|
-
role: m.role === 'assistant' ? 'model' : 'user',
|
|
395
|
-
parts: [{ text: m.content }],
|
|
396
|
-
}));
|
|
397
|
-
const start = nextPoolStart('gemini', keys.length);
|
|
398
|
-
let lastErr;
|
|
399
|
-
for (let i = 0; i < keys.length; i++) {
|
|
400
|
-
const apiKey = keys[(start + i) % keys.length];
|
|
401
|
-
const ctrl = new AbortController();
|
|
402
|
-
const timer = setTimeout(() => ctrl.abort(new Error(`Gemini stream timeout (${STREAM_TIMEOUT_MS}ms)`)), STREAM_TIMEOUT_MS);
|
|
403
|
-
// Encadeia signal externo com o local.
|
|
404
|
-
if (signal) {
|
|
405
|
-
if (signal.aborted)
|
|
406
|
-
ctrl.abort();
|
|
407
|
-
else
|
|
408
|
-
signal.addEventListener('abort', () => ctrl.abort(), { once: true });
|
|
409
|
-
}
|
|
410
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(apiKey)}`;
|
|
411
|
-
try {
|
|
412
|
-
const resp = await fetch(url, {
|
|
413
|
-
method: 'POST',
|
|
414
|
-
headers: { 'content-type': 'application/json' },
|
|
415
|
-
body: JSON.stringify({
|
|
416
|
-
systemInstruction: { parts: [{ text: systemContent }] },
|
|
417
|
-
contents,
|
|
418
|
-
generationConfig: { maxOutputTokens: 8192 },
|
|
419
|
-
}),
|
|
420
|
-
signal: ctrl.signal,
|
|
421
|
-
});
|
|
422
|
-
// Erros de auth/rate-limit → rotaciona chave.
|
|
423
|
-
if (!resp.ok) {
|
|
424
|
-
if (resp.status === 401 || resp.status === 403 || resp.status === 429) {
|
|
425
|
-
lastErr = new Error(`Gemini API erro ${resp.status}`);
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
const txt = await resp.text().catch(() => '');
|
|
429
|
-
throw new Error(`Gemini API erro ${resp.status}: ${txt.slice(0, 200)}`);
|
|
430
|
-
}
|
|
431
|
-
if (!resp.body)
|
|
432
|
-
throw new Error('Gemini: resposta sem body');
|
|
433
|
-
let full = '';
|
|
434
|
-
let usage;
|
|
435
|
-
try {
|
|
436
|
-
await readSseLines(resp.body, (payload) => {
|
|
437
|
-
try {
|
|
438
|
-
const obj = JSON.parse(payload);
|
|
439
|
-
const txt = obj?.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
440
|
-
if (typeof txt === 'string' && txt) {
|
|
441
|
-
onChunk(txt);
|
|
442
|
-
full += txt;
|
|
443
|
-
}
|
|
444
|
-
// usageMetadata aparece no último objeto da stream.
|
|
445
|
-
if (obj?.usageMetadata) {
|
|
446
|
-
const u = obj.usageMetadata;
|
|
447
|
-
usage = {
|
|
448
|
-
inputTokens: u.promptTokenCount ?? 0,
|
|
449
|
-
outputTokens: u.candidatesTokenCount ?? 0,
|
|
450
|
-
totalTokens: u.totalTokenCount ?? 0,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
// ignora linha de keep-alive
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
finally {
|
|
460
|
-
clearTimeout(timer);
|
|
461
|
-
}
|
|
462
|
-
recordUsage(usage);
|
|
463
|
-
return full;
|
|
464
|
-
}
|
|
465
|
-
catch (err) {
|
|
466
|
-
clearTimeout(timer);
|
|
467
|
-
lastErr = err;
|
|
468
|
-
// Timeout → propaga direto (não adianta tentar outra chave).
|
|
469
|
-
if (ctrl.signal.aborted)
|
|
470
|
-
throw err;
|
|
471
|
-
// Auth/rate-limit → rotaciona pra próxima chave do pool
|
|
472
|
-
// (espelhando o comportamento de streamOpenAICompatible).
|
|
473
|
-
const status = err?.status ?? err?.response?.status;
|
|
474
|
-
if (status === 401 || status === 403 || status === 429)
|
|
475
|
-
continue;
|
|
476
|
-
// Outros erros (rede, modelo inválido, payload) propagam direto.
|
|
477
|
-
throw err;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const detail = lastErr?.message ?? String(lastErr);
|
|
481
|
-
throw new Error(`Gemini: todas as ${keys.length} chave(s) do pool falharam (auth/rate-limit). Último erro: ${detail}`);
|
|
482
|
-
}
|
|
483
287
|
/**
|
|
484
288
|
* Streamer do AI Gateway do Core (`POST /api/cli/v1/ai/chat`).
|
|
485
289
|
*
|
|
@@ -494,8 +298,8 @@ async function streamGemini(messages, systemExtra, onChunk, signal) {
|
|
|
494
298
|
* Resposta SSE OpenAI-compatible: `data: {"choices":[{"delta":{"content":"…"}}]}`
|
|
495
299
|
* terminando em `data: [DONE]`. Usage (quando presente) vem em `chunk.usage`
|
|
496
300
|
* (só quando o provider por trás do gateway suporta `stream_options.include_usage`
|
|
497
|
-
* — ex: OpenAI/Groq/NVIDIA;
|
|
498
|
-
*
|
|
301
|
+
* — ex: OpenAI/Groq/NVIDIA; provider que não expõe usage por chunk não manda
|
|
302
|
+
* nada). Se o stream nunca trouxer usage, `recordUsage` simplesmente
|
|
499
303
|
* não é chamado — não inventamos número.
|
|
500
304
|
*/
|
|
501
305
|
async function streamGateway(messages, systemExtra, onChunk, signal) {
|
|
@@ -521,7 +325,7 @@ async function streamGateway(messages, systemExtra, onChunk, signal) {
|
|
|
521
325
|
];
|
|
522
326
|
const ctrl = new AbortController();
|
|
523
327
|
const timer = setTimeout(() => ctrl.abort(new Error(`Neetru AI Gateway stream timeout (${STREAM_TIMEOUT_MS}ms)`)), STREAM_TIMEOUT_MS);
|
|
524
|
-
// Encadeia signal externo com o local
|
|
328
|
+
// Encadeia signal externo com o local.
|
|
525
329
|
if (signal) {
|
|
526
330
|
if (signal.aborted)
|
|
527
331
|
ctrl.abort();
|
|
@@ -613,17 +417,12 @@ function streamFor(model, messages, systemExtra, onChunk) {
|
|
|
613
417
|
switch (model) {
|
|
614
418
|
case 'openai':
|
|
615
419
|
return streamOpenAI(messages, systemExtra, onChunk);
|
|
616
|
-
case 'gemini':
|
|
617
|
-
return streamGemini(messages, systemExtra, onChunk);
|
|
618
420
|
case 'groq':
|
|
619
421
|
return streamGroq(messages, systemExtra, onChunk);
|
|
620
422
|
case 'nvidia':
|
|
621
423
|
return streamNvidia(messages, systemExtra, onChunk);
|
|
622
424
|
case 'gateway':
|
|
623
425
|
return streamGateway(messages, systemExtra, onChunk);
|
|
624
|
-
case 'claude':
|
|
625
|
-
default:
|
|
626
|
-
return streamClaude(messages, systemExtra, onChunk);
|
|
627
426
|
}
|
|
628
427
|
}
|
|
629
428
|
/**
|
|
@@ -632,7 +431,16 @@ function streamFor(model, messages, systemExtra, onChunk) {
|
|
|
632
431
|
*/
|
|
633
432
|
export async function chat(messages, onChunk, preferred) {
|
|
634
433
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user')?.content ?? '';
|
|
635
|
-
|
|
434
|
+
// bug_19d312f3: `defaultModel` pode trazer `claude` ou `gemini` gravado por
|
|
435
|
+
// uma versão antiga — o schema da config não os recusa na leitura para não
|
|
436
|
+
// derrubar todo comando. Aqui o valor desconhecido é recusado com o caminho
|
|
437
|
+
// de volta, em vez de cair calado no `auto`.
|
|
438
|
+
const pedido = preferred ?? config.get('defaultModel') ?? 'auto';
|
|
439
|
+
if (!ehModeloDoAi(pedido)) {
|
|
440
|
+
throw new Error(`O modelo "${String(pedido)}" não existe no neetru ai. Use: ${MODELOS_DO_AI_NA_TELA}. ` +
|
|
441
|
+
'Se ele veio da config, rode `neetru config set defaultModel auto`.');
|
|
442
|
+
}
|
|
443
|
+
const model = routeModel(lastUserMsg, pedido);
|
|
636
444
|
const projectCtx = await loadProjectContext();
|
|
637
445
|
log.dim(`modelo: ${model}`);
|
|
638
446
|
return streamFor(model, messages, projectCtx, onChunk);
|
|
@@ -650,17 +458,9 @@ export async function chat(messages, onChunk, preferred) {
|
|
|
650
458
|
* N tokens quando só 2-3 providers são necessários.
|
|
651
459
|
*/
|
|
652
460
|
export async function chatParallel(prompt, systemExtra, opts) {
|
|
653
|
-
// `gateway` fica de fora do team mode: não é BYOK (sem
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
claude: 'anthropic',
|
|
657
|
-
openai: 'openai',
|
|
658
|
-
gemini: 'gemini',
|
|
659
|
-
groq: 'groq',
|
|
660
|
-
nvidia: 'nvidia',
|
|
661
|
-
};
|
|
662
|
-
const order = ['claude', 'openai', 'gemini', 'groq', 'nvidia'];
|
|
663
|
-
let targets = order.filter((m) => !!resolveKey(providerOf[m]));
|
|
461
|
+
// `gateway` fica de fora do team mode: não é BYOK (sem pool de chaves). A
|
|
462
|
+
// ordem é a da lista única (`PROVEDORES_IA`).
|
|
463
|
+
let targets = PROVEDORES_IA.filter((p) => !!resolveKey(p));
|
|
664
464
|
if (targets.length === 0) {
|
|
665
465
|
throw new Error('Nenhum provedor configurado. Rode: neetru auth setup');
|
|
666
466
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lista ÚNICA de provedores de IA da CLI (bug_19d312f3).
|
|
3
|
+
*
|
|
4
|
+
* Antes havia duas listas que discordavam. O `neetru ai` e o `neetru auth`
|
|
5
|
+
* trabalhavam com anthropic, openai, gemini, groq e nvidia; o `neetru code`,
|
|
6
|
+
* o `neetru ai models` e a config do AI Gateway, com groq, nvidia e openai. A
|
|
7
|
+
* primeira oferecia Anthropic e Gemini, que a casa não usa: IA só de provedor
|
|
8
|
+
* gratuito, e a API da Anthropic e o Gemini custam e estão vetados.
|
|
9
|
+
*
|
|
10
|
+
* Agora todo lugar que enumera provedor lê daqui: o assistente do
|
|
11
|
+
* `neetru auth setup`, a descoberta de chaves, o roteamento e o `/debug:model`
|
|
12
|
+
* do `neetru ai`, o `--provider` do `neetru code`, o catálogo do
|
|
13
|
+
* `neetru ai models`, as chaves aceitas pelo `neetru config set` e o schema da
|
|
14
|
+
* config. O teste `provedores-ia-lista-unica.test.ts` trava a lista e falha se
|
|
15
|
+
* outro arquivo de `src/` declarar uma lista própria.
|
|
16
|
+
*
|
|
17
|
+
* Este módulo não importa nada de propósito: o `index.ts` e o `neetru code` o
|
|
18
|
+
* leem sem carregar o SDK de nenhum provedor.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Provedores com os quais a CLI fala. Groq e NVIDIA NIM primeiro (gratuitos).
|
|
22
|
+
* A OpenAI é paga: o `auto` do `neetru ai` só a escolhe quando nenhum gratuito
|
|
23
|
+
* tem chave (`routeModel` no orchestrator); fora isso, só com `-m openai`.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PROVEDORES_IA: readonly ["groq", "nvidia", "openai"];
|
|
26
|
+
export type ProvedorIa = (typeof PROVEDORES_IA)[number];
|
|
27
|
+
/** Chave da config local que guarda a chave de API de cada provedor. */
|
|
28
|
+
export declare const CHAVE_DO_PROVEDOR: {
|
|
29
|
+
readonly groq: "groqApiKey";
|
|
30
|
+
readonly nvidia: "nvidiaApiKey";
|
|
31
|
+
readonly openai: "openaiApiKey";
|
|
32
|
+
};
|
|
33
|
+
export type ChaveDeProvedor = (typeof CHAVE_DO_PROVEDOR)[ProvedorIa];
|
|
34
|
+
/** Chave da config local que troca o modelo padrão de cada provedor. */
|
|
35
|
+
export declare const CHAVE_DE_MODELO_DO_PROVEDOR: {
|
|
36
|
+
readonly groq: "groqModel";
|
|
37
|
+
readonly nvidia: "nvidiaModel";
|
|
38
|
+
readonly openai: "openaiModel";
|
|
39
|
+
};
|
|
40
|
+
export type ChaveDeModeloDoProvedor = (typeof CHAVE_DE_MODELO_DO_PROVEDOR)[ProvedorIa];
|
|
41
|
+
/** Variável de ambiente de onde a descoberta lê a chave de cada provedor. */
|
|
42
|
+
export declare const VARIAVEL_DO_PROVEDOR: Readonly<Record<ProvedorIa, string>>;
|
|
43
|
+
/** Nome que aparece na tela. */
|
|
44
|
+
export declare const NOME_DO_PROVEDOR: Readonly<Record<ProvedorIa, string>>;
|
|
45
|
+
/** Ordem de `PROVEDORES_IA`, já com o nome da tela: "Groq, NVIDIA NIM, OpenAI". */
|
|
46
|
+
export declare const NOMES_DOS_PROVEDORES: string;
|
|
47
|
+
/**
|
|
48
|
+
* O que `neetru ai -m`, o `/debug:model` e a chave `defaultModel` aceitam: um
|
|
49
|
+
* provedor da lista, o AI Gateway da Neetru (sessão do `neetru login`) ou
|
|
50
|
+
* `auto` (a CLI escolhe entre os provedores com chave).
|
|
51
|
+
*/
|
|
52
|
+
export declare const MODELOS_DO_AI: readonly ["groq", "nvidia", "openai", "gateway", "auto"];
|
|
53
|
+
export type ModeloDoAi = (typeof MODELOS_DO_AI)[number];
|
|
54
|
+
/** Os modelos aceitos, prontos para a tela: "groq | nvidia | openai | gateway | auto". */
|
|
55
|
+
export declare const MODELOS_DO_AI_NA_TELA: string;
|
|
56
|
+
export declare function ehProvedorIa(valor: unknown): valor is ProvedorIa;
|
|
57
|
+
export declare function ehModeloDoAi(valor: unknown): valor is ModeloDoAi;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lista ÚNICA de provedores de IA da CLI (bug_19d312f3).
|
|
3
|
+
*
|
|
4
|
+
* Antes havia duas listas que discordavam. O `neetru ai` e o `neetru auth`
|
|
5
|
+
* trabalhavam com anthropic, openai, gemini, groq e nvidia; o `neetru code`,
|
|
6
|
+
* o `neetru ai models` e a config do AI Gateway, com groq, nvidia e openai. A
|
|
7
|
+
* primeira oferecia Anthropic e Gemini, que a casa não usa: IA só de provedor
|
|
8
|
+
* gratuito, e a API da Anthropic e o Gemini custam e estão vetados.
|
|
9
|
+
*
|
|
10
|
+
* Agora todo lugar que enumera provedor lê daqui: o assistente do
|
|
11
|
+
* `neetru auth setup`, a descoberta de chaves, o roteamento e o `/debug:model`
|
|
12
|
+
* do `neetru ai`, o `--provider` do `neetru code`, o catálogo do
|
|
13
|
+
* `neetru ai models`, as chaves aceitas pelo `neetru config set` e o schema da
|
|
14
|
+
* config. O teste `provedores-ia-lista-unica.test.ts` trava a lista e falha se
|
|
15
|
+
* outro arquivo de `src/` declarar uma lista própria.
|
|
16
|
+
*
|
|
17
|
+
* Este módulo não importa nada de propósito: o `index.ts` e o `neetru code` o
|
|
18
|
+
* leem sem carregar o SDK de nenhum provedor.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Provedores com os quais a CLI fala. Groq e NVIDIA NIM primeiro (gratuitos).
|
|
22
|
+
* A OpenAI é paga: o `auto` do `neetru ai` só a escolhe quando nenhum gratuito
|
|
23
|
+
* tem chave (`routeModel` no orchestrator); fora isso, só com `-m openai`.
|
|
24
|
+
*/
|
|
25
|
+
export const PROVEDORES_IA = ['groq', 'nvidia', 'openai'];
|
|
26
|
+
/** Chave da config local que guarda a chave de API de cada provedor. */
|
|
27
|
+
export const CHAVE_DO_PROVEDOR = {
|
|
28
|
+
groq: 'groqApiKey',
|
|
29
|
+
nvidia: 'nvidiaApiKey',
|
|
30
|
+
openai: 'openaiApiKey',
|
|
31
|
+
};
|
|
32
|
+
/** Chave da config local que troca o modelo padrão de cada provedor. */
|
|
33
|
+
export const CHAVE_DE_MODELO_DO_PROVEDOR = {
|
|
34
|
+
groq: 'groqModel',
|
|
35
|
+
nvidia: 'nvidiaModel',
|
|
36
|
+
openai: 'openaiModel',
|
|
37
|
+
};
|
|
38
|
+
/** Variável de ambiente de onde a descoberta lê a chave de cada provedor. */
|
|
39
|
+
export const VARIAVEL_DO_PROVEDOR = {
|
|
40
|
+
groq: 'GROQ_API_KEY',
|
|
41
|
+
nvidia: 'NVIDIA_API_KEY',
|
|
42
|
+
openai: 'OPENAI_API_KEY',
|
|
43
|
+
};
|
|
44
|
+
/** Nome que aparece na tela. */
|
|
45
|
+
export const NOME_DO_PROVEDOR = {
|
|
46
|
+
groq: 'Groq',
|
|
47
|
+
nvidia: 'NVIDIA NIM',
|
|
48
|
+
openai: 'OpenAI',
|
|
49
|
+
};
|
|
50
|
+
/** Ordem de `PROVEDORES_IA`, já com o nome da tela: "Groq, NVIDIA NIM, OpenAI". */
|
|
51
|
+
export const NOMES_DOS_PROVEDORES = PROVEDORES_IA.map((p) => NOME_DO_PROVEDOR[p]).join(', ');
|
|
52
|
+
/**
|
|
53
|
+
* O que `neetru ai -m`, o `/debug:model` e a chave `defaultModel` aceitam: um
|
|
54
|
+
* provedor da lista, o AI Gateway da Neetru (sessão do `neetru login`) ou
|
|
55
|
+
* `auto` (a CLI escolhe entre os provedores com chave).
|
|
56
|
+
*/
|
|
57
|
+
export const MODELOS_DO_AI = [...PROVEDORES_IA, 'gateway', 'auto'];
|
|
58
|
+
/** Os modelos aceitos, prontos para a tela: "groq | nvidia | openai | gateway | auto". */
|
|
59
|
+
export const MODELOS_DO_AI_NA_TELA = MODELOS_DO_AI.join(' | ');
|
|
60
|
+
export function ehProvedorIa(valor) {
|
|
61
|
+
return typeof valor === 'string' && PROVEDORES_IA.includes(valor);
|
|
62
|
+
}
|
|
63
|
+
export function ehModeloDoAi(valor) {
|
|
64
|
+
return typeof valor === 'string' && MODELOS_DO_AI.includes(valor);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=provedores.js.map
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -16,7 +16,14 @@ export declare class CliApiError extends Error {
|
|
|
16
16
|
* por quem faz vários envios em sequência (ex.: `neetru docs sync`).
|
|
17
17
|
*/
|
|
18
18
|
readonly retryAfterSeconds?: number;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Campos EXTRAS do erro canônico `{ error: { code, message, ...detalhes } }`
|
|
21
|
+
* (ex.: core#732 — `instanceId`, `instanceCreatedAt`, `docCreatedAt`,
|
|
22
|
+
* `outrosServidores`, `comandoConferencia` no force-cleanup). Ausente quando
|
|
23
|
+
* o corpo só traz `code`/`message`. Quem usa valida o tipo de cada campo.
|
|
24
|
+
*/
|
|
25
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
26
|
+
constructor(status: number, code: string, message: string, correlationId?: string, retryAfterSeconds?: number, details?: Readonly<Record<string, unknown>>);
|
|
20
27
|
}
|
|
21
28
|
/** `Retry-After` em segundos (aceita número ou data HTTP). undefined se ausente/ilegível. */
|
|
22
29
|
export declare function parseRetryAfter(value: string | null | undefined, now?: number): number | undefined;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -17,13 +17,22 @@ export class CliApiError extends Error {
|
|
|
17
17
|
* por quem faz vários envios em sequência (ex.: `neetru docs sync`).
|
|
18
18
|
*/
|
|
19
19
|
retryAfterSeconds;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Campos EXTRAS do erro canônico `{ error: { code, message, ...detalhes } }`
|
|
22
|
+
* (ex.: core#732 — `instanceId`, `instanceCreatedAt`, `docCreatedAt`,
|
|
23
|
+
* `outrosServidores`, `comandoConferencia` no force-cleanup). Ausente quando
|
|
24
|
+
* o corpo só traz `code`/`message`. Quem usa valida o tipo de cada campo.
|
|
25
|
+
*/
|
|
26
|
+
details;
|
|
27
|
+
constructor(status, code, message, correlationId, retryAfterSeconds, details) {
|
|
21
28
|
super(message);
|
|
22
29
|
this.name = 'CliApiError';
|
|
23
30
|
this.status = status;
|
|
24
31
|
this.code = code;
|
|
25
32
|
this.correlationId = correlationId;
|
|
26
33
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
34
|
+
if (details && Object.keys(details).length > 0)
|
|
35
|
+
this.details = details;
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
/** `Retry-After` em segundos (aceita número ou data HTTP). undefined se ausente/ilegível. */
|
|
@@ -182,6 +191,7 @@ export async function apiRequest(path, opts = {}) {
|
|
|
182
191
|
const errBody = parsed;
|
|
183
192
|
let code = `http_${response.status}`;
|
|
184
193
|
let message = `Erro HTTP ${response.status}`;
|
|
194
|
+
let details;
|
|
185
195
|
if (errBody?.error) {
|
|
186
196
|
if (typeof errBody.error === 'string') {
|
|
187
197
|
// Formato RFC. Auditoria cli/v1 2026-09-29: o 401 do `requireCliAuth`
|
|
@@ -196,6 +206,12 @@ export async function apiRequest(path, opts = {}) {
|
|
|
196
206
|
// Formato interno Neetru
|
|
197
207
|
code = errBody.error.code ?? code;
|
|
198
208
|
message = errBody.error.message ?? message;
|
|
209
|
+
// Campos além de `code`/`message` (core#732): vão em `details`, crus.
|
|
210
|
+
const { code: _c, message: _m, ...extras } = errBody.error;
|
|
211
|
+
void _c;
|
|
212
|
+
void _m;
|
|
213
|
+
if (Object.keys(extras).length > 0)
|
|
214
|
+
details = extras;
|
|
199
215
|
}
|
|
200
216
|
}
|
|
201
217
|
else if (typeof parsed === 'string' && parsed.trim() !== '') {
|
|
@@ -242,7 +258,7 @@ export async function apiRequest(path, opts = {}) {
|
|
|
242
258
|
}
|
|
243
259
|
// LABS #6 (2026-09-28): captura o x-correlation-id do Core p/ o rodapé do erro.
|
|
244
260
|
const correlationId = response.headers.get('x-correlation-id') ?? undefined;
|
|
245
|
-
throw new CliApiError(response.status, code, message, correlationId, parseRetryAfter(response.headers.get('retry-after')));
|
|
261
|
+
throw new CliApiError(response.status, code, message, correlationId, parseRetryAfter(response.headers.get('retry-after')), details);
|
|
246
262
|
}
|
|
247
263
|
return parsed;
|
|
248
264
|
}
|