primocode 11.4.0 → 11.5.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.
Files changed (85) hide show
  1. package/LICENSES/edit2-terceiros.md +31 -0
  2. package/LICENSES/models-dev-MIT.txt +25 -0
  3. package/README.md +2 -2
  4. package/editor/SKILL.md +24 -0
  5. package/editor/THIRD_PARTY_NOTICES.md +1 -0
  6. package/editor/assets/baixar.json +9 -1
  7. package/editor/helpers/df_limpar.py +29 -0
  8. package/editor/helpers/edit2.py +155 -0
  9. package/editor/helpers/enquadrar_camera.py +77 -0
  10. package/editor/helpers/face_layout.py +20 -0
  11. package/editor/helpers/face_mediapipe.py +147 -0
  12. package/editor/helpers/instalar_df.sh +7 -0
  13. package/editor/helpers/legenda_ass.py +124 -0
  14. package/editor/helpers/look_ffmpeg.py +105 -0
  15. package/editor/helpers/motor_palavras.py +157 -0
  16. package/editor/helpers/primeiro_passe.py +214 -0
  17. package/editor/helpers/som_edit2.py +119 -0
  18. package/editor/helpers/transcribe.py +31 -2
  19. package/editor/pyproject.toml +9 -0
  20. package/editor/references/edit2.md +22 -0
  21. package/editor/uv.lock +3342 -214
  22. package/kits/3d/LICENSES/README.md +11 -0
  23. package/kits/3d/animacoes.js +87 -0
  24. package/kits/3d/base.js +44 -0
  25. package/kits/3d/catalogo.js +38 -0
  26. package/kits/3d/cenario.js +162 -0
  27. package/kits/3d/exemplos/asfalto.mjs +91 -0
  28. package/kits/3d/maquinas.js +97 -0
  29. package/kits/3d/montador.js +142 -0
  30. package/kits/3d/package.json +1 -0
  31. package/kits/3d/personagem.js +86 -0
  32. package/kits/3d/remotion/Maquete.jsx +42 -0
  33. package/kits/3d/remotion/Root.jsx +10 -0
  34. package/kits/3d/render.mjs +73 -0
  35. package/lib/cli/comando-api.js +30 -8
  36. package/lib/cli/modelos-lista.js +58 -0
  37. package/lib/cli/sessao.js +11 -0
  38. package/lib/cli/subcomandos.js +3 -2
  39. package/lib/outra-pasta.js +1 -1
  40. package/lib/permissao-sistema.js +60 -0
  41. package/lib/permission/perigo.js +1 -0
  42. package/lib/permission/permissao.js +1 -0
  43. package/lib/permission/sem-perguntar.js +29 -0
  44. package/lib/plugin/desktop.js +5 -4
  45. package/lib/plugin/estudio/cota.js +4 -2
  46. package/lib/plugin/estudio/dialogo3d.js +71 -0
  47. package/lib/plugin/estudio/effectcraft.js +1 -1
  48. package/lib/plugin/estudio/glifos.js +2 -0
  49. package/lib/plugin/estudio/gsap-local.js +43 -0
  50. package/lib/plugin/estudio/index.js +63 -10
  51. package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
  52. package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
  53. package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
  54. package/lib/plugin/estudio/maquina-diretor.js +1 -1
  55. package/lib/plugin/estudio/maquina-elementos.js +2 -2
  56. package/lib/plugin/estudio/maquina.js +26 -8
  57. package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
  58. package/lib/plugin/estudio/motion-plano/critico.js +59 -0
  59. package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
  60. package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
  61. package/lib/plugin/estudio/motion-plano/index.js +75 -0
  62. package/lib/plugin/estudio/motion-plano/llm.js +59 -0
  63. package/lib/plugin/estudio/motion-plano/motor.js +50 -0
  64. package/lib/plugin/estudio/motion-plano/passos.js +38 -0
  65. package/lib/plugin/estudio/motion-plano/pele.js +36 -0
  66. package/lib/plugin/estudio/motion-plano/plano.js +152 -0
  67. package/lib/plugin/estudio/motion-plano/regua.js +100 -0
  68. package/lib/plugin/estudio/personagem.js +3 -1
  69. package/lib/plugin/estudio/referencias.js +1 -1
  70. package/lib/provider/catalogo.js +161 -0
  71. package/lib/provider/local/catalogo-embutido.json.gz +0 -0
  72. package/lib/provider/local/motor.js +140 -27
  73. package/lib/provider/local/orcamento.js +193 -0
  74. package/lib/provider/local/provedor.js +127 -14
  75. package/lib/provider/local/recorte.js +170 -0
  76. package/lib/provider/local/revezamento.js +121 -0
  77. package/lib/session/runner/act.js +28 -3
  78. package/lib/skill/catalogo.js +5 -1
  79. package/lib/tool/tools.js +5 -1
  80. package/package.json +1 -1
  81. package/vendor/fontes/ttf/ORIGEM.txt +1 -0
  82. package/vendor/fontes/ttf/inter-800.ttf +0 -0
  83. package/vendor/gsap/ORIGEM.txt +6 -0
  84. package/vendor/gsap/gsap.min.js +11 -0
  85. package/voz/primo_voz/chat.html +167 -5
@@ -19,6 +19,9 @@ const entrada = require('./entrada.js');
19
19
  const provedor = require('./provedor.js');
20
20
  const catalogo = require('../../skill/catalogo.js');
21
21
  const chamadaTorta = require('../../chamada-torta');
22
+ const catalogoModelos = require('../catalogo');
23
+ const orcamento = require('./orcamento.js');
24
+ const revezamento = require('./revezamento.js');
22
25
 
23
26
  // A conversa pode ocupar isto, em tokens, além do prompt e das ferramentas.
24
27
  // Folgado: os modelos de hoje têm 128 mil ou mais de contexto.
@@ -45,8 +48,10 @@ function ferramentasDoCliente() {
45
48
  }
46
49
  }
47
50
  if (!lista.some((t) => t.function.name === 'finish') && doServidor.has('finish')) lista.push(doServidor.get('finish'));
48
- return lista;
51
+ // mais_ferramentas só existe quando o recorte tira ferramentas (recorte.js monta o schema dela)
52
+ return lista.filter((t) => t.function.name !== 'mais_ferramentas');
49
53
  }
54
+ let ULTIMO_PLANO = null; // o que a última volta mandou (nível, tokens): o teste e a prova leem daqui
50
55
  let FERRAMENTAS = null;
51
56
  let FERRAMENTAS_SUB = null;
52
57
  function ferramentas(subagent) {
@@ -57,6 +62,28 @@ function ferramentas(subagent) {
57
62
  return subagent ? FERRAMENTAS_SUB : FERRAMENTAS;
58
63
  }
59
64
 
65
+ /* ── O QUE O MODELO ESCOLHIDO SABE FAZER ─────────────────────────────────
66
+ * Vem do catálogo (models.dev): ferramentas, visão, contexto, saída máxima.
67
+ * Modelo que o catálogo não conhece (um gateway, um modelo novo) tem o
68
+ * benefício da dúvida: tudo ligado, como era antes. */
69
+ function perfil(p = provedor.atual()) {
70
+ const cap = p && p.noCatalogo ? catalogoModelos.capacidades(p.noCatalogo, p.modelo) : null;
71
+ const saida = cap && cap.saida > 0 ? Math.min(SAIDA_MAXIMA, cap.saida) : SAIDA_MAXIMA;
72
+ // a conversa usa até metade do contexto; o resto fica para prompt, ferramentas e resposta
73
+ const teto = cap && cap.contexto > 0 ? Math.min(TETO_CONVERSA, Math.floor(cap.contexto * 0.5)) : TETO_CONVERSA;
74
+ return { conhecido: !!cap, ferramentas: cap ? cap.ferramentas : true, visao: cap ? cap.visao : true, saida, teto };
75
+ }
76
+
77
+ /** Modelo sem visão não recebe imagem: vira um aviso em texto (o pedido não quebra). */
78
+ function semImagens(messages, pf) {
79
+ if (pf.visao) return messages;
80
+ return messages.map((m) => {
81
+ if (!m || !Array.isArray(m.content)) return m;
82
+ const partes = m.content.map((x) => (x && x.type === 'image_url' ? { type: 'text', text: '[imagem omitida: este modelo não vê imagens]' } : x));
83
+ return { ...m, content: partes };
84
+ });
85
+ }
86
+
60
87
  /** O erro da API, no formato que o loop já sabe tratar (429 espera, 401 avisa). */
61
88
  async function erroHttp(r, p) {
62
89
  let msg = '';
@@ -72,8 +99,17 @@ async function erroHttp(r, p) {
72
99
  if (r.status === 401 || r.status === 403) {
73
100
  return new Error(`${r.status} ${quem} recusou a chave. Rode /api para colar outra, ou /api off para voltar ao servidor.`);
74
101
  }
75
- const espera = r.headers && r.headers.get && r.headers.get('retry-after');
76
- return new Error(`${r.status} ${quem}: ${String(msg).slice(0, 300)}${espera ? ` Please try again in ${espera}s.` : ''}`);
102
+ const h = (n) => (r.headers && r.headers.get && r.headers.get(n)) || null;
103
+ const espera = h('retry-after');
104
+ const e = new Error(`${r.status} ${quem}: ${String(msg).slice(0, 300)}${espera ? ` Please try again in ${espera}s.` : ''}`);
105
+ // o que o retry de TPM precisa, sem reler o texto: status, quanto esperar e o limite que o provedor contou
106
+ e.status = r.status;
107
+ e.esperaMs = orcamento.duracaoMs(espera) || orcamento.duracaoMs(h('x-ratelimit-reset-tokens')) || orcamento.duracaoMs((/try again in ([0-9hms.]+)/i.exec(msg) || [])[1]);
108
+ const lim = /Limit (\d+)/.exec(msg);
109
+ e.limiteTpm = lim ? Number(lim[1]) : null;
110
+ e.tpm = (r.status === 413 || r.status === 429) && /tokens per minute|\bTPM\b|request too large|reduce your message size/i.test(msg);
111
+ e.tpd = /tokens per day|\bTPD\b/i.test(msg);
112
+ return e;
77
113
  }
78
114
 
79
115
  function cabecalhos(p) {
@@ -100,8 +136,7 @@ async function completar(messages, { maxTokens = 600, temperatura = 0.2 } = {})
100
136
  * Lê o stream SSE do /chat/completions. Chama `aoPedaco(delta, fim)` a cada
101
137
  * pedaço. Resolve quando acaba ou quando `sinal` corta.
102
138
  */
103
- async function lerStream(corpo, { sinal, aoPedaco }) {
104
- const p = provedor.atual();
139
+ async function lerStream(corpo, { sinal, aoPedaco, p = provedor.atual() }) {
105
140
  let r;
106
141
  try {
107
142
  r = await fetch(`${p.base}/chat/completions`, {
@@ -158,45 +193,122 @@ function juntarChamadas(acumuladas, pedacos) {
158
193
  return acumuladas;
159
194
  }
160
195
 
196
+ /* ── 413/429 POR TOKENS POR MINUTO ───────────────────────────────────────
197
+ * O provedor grátis diz quanto esperar (retry-after, x-ratelimit-reset-tokens). A pessoa lê uma
198
+ * frase simples, nunca o erro cru; a tentativa seguinte leva um pedido menor (um nível a mais de
199
+ * aperto em orcamento.planejar) e o nível fica lembrado para as próximas voltas. Só erro HTTP
200
+ * (antes de qualquer pedaço da resposta) é repetido: no meio do stream, nada de duplicar texto. */
201
+ const MAX_TENTATIVAS_TPM = 3;
202
+ const ESPERA_MAXIMA_MS = 70000;
203
+
204
+ function dormir(ms, sinal) {
205
+ return new Promise((ok) => {
206
+ const t = setTimeout(ok, ms);
207
+ if (sinal) sinal.addEventListener('abort', () => { clearTimeout(t); ok(); }, { once: true });
208
+ });
209
+ }
210
+
211
+ async function comRetryDeTpm(montar, p, emit, sinal, enviar, dormirFn = dormir, revezar = null) {
212
+ let nivel = p ? orcamento.pisoDeNivel(p.id, p.modelo) : 0;
213
+ for (let tentativa = 0; ; tentativa++) {
214
+ const corpo = montar(nivel);
215
+ try { return await enviar(corpo); } catch (e) {
216
+ if (!e || !e.tpm || (sinal && sinal.aborted)) throw e;
217
+ // outro provedor grátis livre? Segue nele em vez de esperar (o erro veio antes de qualquer pedaço)
218
+ const outro = revezar && revezar(e);
219
+ if (outro) { p = outro; nivel = orcamento.pisoDeNivel(p.id, p.modelo); continue; }
220
+ if (e.tpd) throw new Error(`429 O limite de hoje do modelo grátis acabou. Tente amanhã, ou troque a chave ou o modelo com /api. (tokens per day)`);
221
+ if (e.limiteTpm && p) orcamento.aprender(p.id, p.modelo, e.limiteTpm);
222
+ if (tentativa >= MAX_TENTATIVAS_TPM) {
223
+ const s = Math.max(1, Math.ceil((e.esperaMs || 30000) / 1000));
224
+ throw new Error(`429 O modelo grátis pediu uma pausa de ${s}s e o pedido ainda não coube. Please try again in ${s}s.`);
225
+ }
226
+ // 413 sem tempo no header: o pedido era grande demais, não é fila — recorta já.
227
+ const espera = Math.min(ESPERA_MAXIMA_MS, e.esperaMs ? e.esperaMs + 500 : 0);
228
+ if (espera > 0) emit({ aviso: orcamento.fraseDePausa(espera) });
229
+ else emit({ aviso: 'O modelo grátis tem um limite por minuto: mando um pedido menor.' });
230
+ if (espera > 0) await dormirFn(espera, sinal);
231
+ const usado = Number.isInteger(montar.usado) ? montar.usado : nivel;
232
+ nivel = Math.min(orcamento.NIVEL_MAXIMO, Math.max(nivel, usado) + 1);
233
+ if (p) orcamento.subirPiso(p.id, p.modelo, nivel);
234
+ }
235
+ }
236
+ }
237
+
161
238
  /* ── /api/act/stream ─────────────────────────────────────────────────── */
162
239
  async function requestAct(_servidor, body, emit) {
163
- const { messages: convo = [], prompt, history, effort, projectDir, projectName, subagent, sinal } = body || {};
240
+ const { messages: convo = [], prompt, history, effort, projectDir, projectName, subagent, sinal, modo } = body || {};
164
241
  const pedido = [...convo].reverse().find((m) => m && m.role === 'user');
165
242
  const texto = String((pedido && pedido.content) || prompt || '');
166
243
  const forcar = effort && ESFORCO_PESQUISA.includes(String(effort)) ? ['pesquisa'] : [];
167
244
 
168
- // Todas, sempre: o recorte por assunto do servidor existe para caber em
169
- // 6.000 tokens por minuto, e ainda não conhece o Estúdio novo (estudio_*)
170
- // — com ele, um pedido de vídeo chegaria sem a ferramenta de vídeo.
171
- const tools = ferramentas(subagent);
172
- const sistema = subagent
173
- ? cerebro.SYSTEM_SUB
174
- : entrada.montarPrompt(cerebro.SYSTEM_NUCLEO, cerebro.SECOES, texto, { forcar, jaAprendeu: entrada.jaAprendeu(convo) });
175
- const messages = [{ role: 'system', content: sistema }];
176
- if (projectDir) {
177
- messages.push({ role: 'system', content: `[Projeto: ${projectName || 'sem nome'}]\n[Pasta: ${projectDir}]\n`
178
- + 'Todo caminho que você passar para write_file/read_file/list_dir é relativo a esta pasta. '
179
- + 'Use caminhos relativos simples (ex: "index.html", "src/app.js"). Nunca use caminho absoluto.' });
245
+ /* O que vai no pedido. Provedor sem limite de TPM: todas as ferramentas, como sempre.
246
+ Provedor com limite (Groq grátis ~8k/min): só os assuntos do pedido e do modo, descrições
247
+ e prompt mais curtos se ainda não couber (lib/provider/local/orcamento.js). */
248
+ const base = provedor.atual(); // o que a pessoa escolheu; o revezamento parte dele
249
+ const temImagem = convo.some((m) => m && Array.isArray(m.content) && m.content.some((x) => x && x.type === 'image_url'));
250
+ let p = base;
251
+ if (base && !base.cli) {
252
+ const e0 = revezamento.escolher(base, { visao: temImagem });
253
+ if (e0.trocou) {
254
+ p = e0.p;
255
+ emit({ aviso: p.id === base.id ? `Tem imagem; usei o ${p.modelo}, que enxerga.` : revezamento.frase(base, p, temImagem ? 'visao' : 'pausa') });
256
+ }
180
257
  }
181
- const conversa = convo.length ? convo : [...(history || []), { role: 'user', content: String(prompt || '') }];
182
- messages.push(...entrada.caberNoTeto(conversa.filter((m) => m && m.role), TETO_CONVERSA).messages);
258
+ let pf = perfil(p);
259
+ const revezar = (e) => {
260
+ if (!base || base.cli) return null;
261
+ revezamento.marcarOcupado(p.id, e.esperaMs, { dia: !!e.tpd });
262
+ const r = revezamento.escolher(base, { visao: temImagem });
263
+ if (!r.trocou || r.p.id === p.id) return null;
264
+ const de = p; p = r.p; pf = perfil(p);
265
+ emit({ aviso: revezamento.frase(de, p) });
266
+ return p;
267
+ };
268
+ const montar = (nivelMin = 0) => {
269
+ const limite = p ? orcamento.limiteTpm(p.id, p.modelo) : 0;
270
+ const conversa = convo.length ? convo : [...(history || []), { role: 'user', content: String(prompt || '') }];
271
+ const aparar = (m, sobra) => semImagens(entrada.caberNoTeto(m, sobra).messages, pf);
272
+ const mensagens = semImagens(entrada.caberNoTeto(conversa.filter((m) => m && m.role), pf.teto).messages, pf);
273
+ const extra = [];
274
+ if (projectDir) {
275
+ extra.push({ role: 'system', content: `[Projeto: ${projectName || 'sem nome'}]\n[Pasta: ${projectDir}]\n`
276
+ + 'Todo caminho que você passar para write_file/read_file/list_dir é relativo a esta pasta. '
277
+ + 'Use caminhos relativos simples (ex: "index.html", "src/app.js"). Nunca use caminho absoluto.' });
278
+ }
279
+ if (!pf.ferramentas) {
280
+ extra.push({ role: 'system', content: 'Este modelo não tem ferramentas neste momento: você não pode criar nem editar arquivos, nem rodar comandos. Responda só em texto e diga ao usuário o que ele mesmo pode fazer; se ele quiser que você aja, peça para trocar o modelo com /api modelos.' });
281
+ }
282
+ // modelo sem ferramentas só conversa: mandar `tools` a ele dá erro 400 em quase todo provedor
283
+ const todas = pf.ferramentas ? ferramentas(subagent) : [];
284
+ const plano = orcamento.planejar({ todas, texto, modo, mensagens, nucleo: cerebro.SYSTEM_NUCLEO, secoes: cerebro.SECOES,
285
+ limite: pf.ferramentas ? limite : 0, saidaMax: pf.saida, nivelMin, jaAprendeu: entrada.jaAprendeu(convo),
286
+ extraTokens: orcamento.tokensDe(extra), aparar, forcar, sistemaFixo: subagent ? cerebro.SYSTEM_SUB : null });
287
+ const msgs = [{ role: 'system', content: plano.sistema }, ...(projectDir ? [extra[0]] : []), ...plano.mensagens, ...extra.slice(projectDir ? 1 : 0)];
288
+ const corpo = { messages: msgs, ...(plano.tools.length ? { tools: plano.tools, tool_choice: 'auto' } : {}), temperature: 0.1, max_tokens: Math.min(pf.saida, plano.saida) };
289
+ montar.usado = plano.nivel;
290
+ ULTIMO_PLANO = { nivel: plano.nivel, recortou: plano.recortou, tokens: plano.tokens, limite };
291
+ if (process.env.PRIMOCODE_TRACO === '1') process.stderr.write(` [orçamento] limite ${limite || 'sem'} · nível ${plano.nivel} · ${plano.tools.length} ferramentas · ${JSON.stringify(plano.tokens)}\n`);
292
+ return corpo;
293
+ };
183
294
 
184
295
  let texto_ = '';
185
296
  const chamadas = [];
186
- await lerStream({ messages, tools, tool_choice: 'auto', temperature: 0.1, max_tokens: SAIDA_MAXIMA }, {
187
- sinal,
297
+ await comRetryDeTpm(montar, p, emit, sinal, (corpo) => lerStream(corpo, {
298
+ sinal, p,
188
299
  aoPedaco: (delta) => {
189
300
  if (delta.tool_calls) juntarChamadas(chamadas, delta.tool_calls);
190
301
  if (delta.content && !chamadas.length) { texto_ += delta.content; emit({ delta: delta.content }); }
191
302
  },
192
- });
303
+ }), undefined, revezar);
193
304
  chamadas.forEach((tc, i) => {
194
305
  if (!tc || !tc.name) return;
195
306
  /* JSON quebrado: tenta recuperar; se não der, o act.js devolve o erro
196
307
  (com os argumentos crus) ao modelo — ver lib/chamada-torta.js. */
197
308
  const rec = chamadaTorta.recuperar(tc.arguments);
198
309
  const args = rec.ok ? rec.args : chamadaTorta.marcar(tc.arguments);
199
- emit({ tool_call: { id: tc.id || `local_${Date.now()}_${i}`, name: tc.name, arguments: args } });
310
+ // gpt-oss no Groq às vezes cola o canal do formato dele no nome ('estudio_motion<|channel|>commentary')
311
+ emit({ tool_call: { id: tc.id || `local_${Date.now()}_${i}`, name: tc.name.replace(/<\|.*$/, '').trim(), arguments: args } });
200
312
  });
201
313
  if (texto_) emit({ assistant_message: { content: texto_ } });
202
314
  emit({ done: true });
@@ -207,9 +319,10 @@ async function requestChat(_servidor, body, emit) {
207
319
  const { messages, prompt, history, sinal } = body || {};
208
320
  const turnos = Array.isArray(messages) ? messages
209
321
  : [...(Array.isArray(history) ? history : []), ...(prompt ? [{ role: 'user', content: prompt }] : [])];
322
+ const pf = perfil();
210
323
  await lerStream({
211
- messages: [{ role: 'system', content: cerebro.SYSTEM_CHAT }, ...turnos.filter((m) => m && m.role)],
212
- temperature: 0.4, max_tokens: 1200,
324
+ messages: semImagens([{ role: 'system', content: cerebro.SYSTEM_CHAT }, ...turnos.filter((m) => m && m.role)], pf),
325
+ temperature: 0.4, max_tokens: Math.min(1200, pf.saida),
213
326
  }, { sinal, aoPedaco: (delta) => { if (delta.content) emit({ delta: delta.content }); } });
214
327
  emit({ done: true });
215
328
  }
@@ -264,4 +377,4 @@ async function requestPlano(_servidor, { prompt } = {}) {
264
377
  }
265
378
  }
266
379
 
267
- module.exports = { requestAct, requestChat, requestResumo, requestPlano, juntarChamadas, lerPassos, ferramentas, ativo: () => !!provedor.atual() };
380
+ module.exports = { comRetryDeTpm, ultimoPlano: () => ULTIMO_PLANO, perfil, semImagens, requestAct, requestChat, requestResumo, requestPlano, juntarChamadas, lerPassos, ferramentas, ativo: () => !!provedor.atual() };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * orcamento.js — quanto cabe num pedido, para provedor com limite de tokens por minuto (TPM).
3
+ *
4
+ * O plano grátis do Groq aceita ~8.000 tokens por minuto. Só a definição das
5
+ * ~65 ferramentas do cliente já passa disso (HTTP 413), então o modo agente
6
+ * nem começava. Aqui mora a conta:
7
+ *
8
+ * limite do provedor → quanto sobra para ferramentas + prompt + conversa
9
+ * não coube? → descrições curtas, e só depois o prompt mais curto
10
+ *
11
+ * O limite NÃO fica espalhado no código. Ordem de quem manda:
12
+ * 1. PRIMOCODE_TPM (variável de ambiente, vale para tudo)
13
+ * 2. ~/.primocode/limites.json — { "groq": 8000, "groq/openai/gpt-oss-20b": 6000, "openai": 0 }
14
+ * (0 = sem limite: comportamento antigo, todas as ferramentas)
15
+ * 3. o que o provedor contou num erro ("Limit 8000"), lembrado até fechar o programa
16
+ * 4. LIMITES_GRATIS, abaixo — só provedores cujo plano de entrada é conhecido por ser apertado
17
+ * Provedor fora dessas listas = sem limite.
18
+ *
19
+ * Puro de propósito (fora a leitura do limites.json): o teste mede o orçamento sem chave de API.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const fs = require('fs');
25
+ const os = require('os');
26
+ const path = require('path');
27
+ const entrada = require('./entrada.js');
28
+ const recorte = require('./recorte.js');
29
+
30
+ /** O único lugar com número de provedor. Quem paga e tem limite maior troca no limites.json. */
31
+ const LIMITES_GRATIS = { groq: 8000 };
32
+
33
+ /** Fração do limite que UM pedido pode usar (entrada + saída). Menos de 1 de propósito: o minuto é dividido
34
+ * entre todas as voltas do agente, e um pedido que come o minuto inteiro faz a volta seguinte esperar. */
35
+ const FRACAO_PEDIDO = 0.6;
36
+ /** Fração do limite reservada para a resposta do modelo, e o mínimo dela. */
37
+ const FRACAO_SAIDA = 0.2;
38
+ const SAIDA_MINIMA = 768;
39
+ /** A conversa nunca é espremida abaixo disto, mesmo no recorte mais fundo. */
40
+ const CONVERSA_MINIMA = 1500;
41
+
42
+ const PISOS = new Map(); // "provedor/modelo" → nível de aperto que já se mostrou necessário
43
+ /** Caracteres por token da conta de orçamento. 4 é o de sempre (e erra para o lado seguro em JSON de schema);
44
+ * trocável em PRIMOCODE_CHARS_POR_TOKEN sem mexer no código. */
45
+ const CHARS_POR_TOKEN = Number(process.env.PRIMOCODE_CHARS_POR_TOKEN) > 0 ? Number(process.env.PRIMOCODE_CHARS_POR_TOKEN) : 4;
46
+ function tokensDe(x) {
47
+ const texto = typeof x === 'string' ? x : JSON.stringify(x || '');
48
+ return Math.ceil(texto.length / CHARS_POR_TOKEN);
49
+ }
50
+
51
+ const APRENDIDOS = new Map(); // "provedor/modelo" → limite que o provedor disse num erro
52
+
53
+ const ARQUIVO = () => path.join(os.homedir(), '.primocode', 'limites.json');
54
+
55
+ function lerArquivo() {
56
+ try {
57
+ const j = JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8'));
58
+ return j && typeof j === 'object' ? j : {};
59
+ } catch { return {}; }
60
+ }
61
+
62
+ function numeroOuNull(v) {
63
+ const n = Number(v);
64
+ return Number.isFinite(n) && n >= 0 ? n : null;
65
+ }
66
+
67
+ /** TPM do provedor/modelo, ou 0 se não há limite conhecido (= comportamento antigo). */
68
+ function limiteTpm(provedorId, modelo) {
69
+ const env = numeroOuNull(process.env.PRIMOCODE_TPM);
70
+ if (env !== null && process.env.PRIMOCODE_TPM !== '') return env;
71
+ const arq = lerArquivo();
72
+ for (const chave of [`${provedorId}/${modelo}`, provedorId]) {
73
+ const n = numeroOuNull(arq[chave]);
74
+ if (n !== null) return n;
75
+ }
76
+ const visto = APRENDIDOS.get(`${provedorId}/${modelo}`);
77
+ if (visto) return visto;
78
+ return LIMITES_GRATIS[provedorId] || 0;
79
+ }
80
+
81
+ /** O provedor disse o limite dele no erro: passa a valer (só se for menor que o que se achava). */
82
+ function aprender(provedorId, modelo, limite) {
83
+ const n = numeroOuNull(limite);
84
+ if (!n) return;
85
+ const chave = `${provedorId}/${modelo}`;
86
+ const atual = limiteTpm(provedorId, modelo);
87
+ if (!atual || n < atual) APRENDIDOS.set(chave, n);
88
+ }
89
+
90
+ function pisoDeNivel(provedorId, modelo) { return PISOS.get(`${provedorId}/${modelo}`) || 0; }
91
+ function subirPiso(provedorId, modelo, nivel) {
92
+ const k = `${provedorId}/${modelo}`;
93
+ if (nivel > (PISOS.get(k) || 0)) PISOS.set(k, nivel);
94
+ }
95
+ function esquecerAprendidos() { APRENDIDOS.clear(); PISOS.clear(); }
96
+
97
+ /* ── os níveis de aperto, do mais leve ao mais fundo ─────────────────────
98
+ * 0 tudo completo 1 descrições até 350 caracteres
99
+ * 2 descrições de 1 frase 3 prompt só com a seção do Estúdio
100
+ * 4 prompt só com o núcleo 5 assunto secundário sai (motion manda sobre estúdio)
101
+ * */
102
+ const NIVEL_MAXIMO = 5;
103
+ const TAMANHO_DESCRICAO = [Infinity, 350, 160, 160, 160, 160];
104
+
105
+ function encurtar(tools, max) {
106
+ if (!isFinite(max)) return tools;
107
+ return tools.map((t) => {
108
+ const d = t && t.function && t.function.description;
109
+ if (!d || d.length <= max) return t;
110
+ const corte = d.slice(0, max);
111
+ const ponto = Math.max(corte.lastIndexOf('. '), corte.lastIndexOf('; '));
112
+ const curta = ponto > max * 0.4 ? corte.slice(0, ponto + 1) : `${corte.replace(/\s+\S*$/, '')}…`;
113
+ return { ...t, function: { ...t.function, description: curta } };
114
+ });
115
+ }
116
+
117
+ function sistemaDoNivel(nucleo, secoes, texto, opts, nivel, grupos) {
118
+ if (opts.sistemaFixo) return opts.sistemaFixo; // subagente: o prompt dele é curto e não tem seções
119
+ if (nivel <= 2) return entrada.montarPrompt(nucleo, secoes, texto, opts);
120
+ const partes = [nucleo];
121
+ if (nivel === 3 && secoes && secoes.estudio && (grupos.has('estudio') || grupos.has('after-effects'))) partes.push(secoes.estudio);
122
+ return partes.join('\n\n');
123
+ }
124
+
125
+ /**
126
+ * Decide o que vai no pedido.
127
+ *
128
+ * `todas`: as ferramentas completas. `mensagens`: a conversa já aparada.
129
+ * `limite`: TPM (0 = sem limite). `nivelMin`: de onde começar (o retry sobe).
130
+ * Devolve { tools, sistema, mensagens, nivel, grupos, recortou, saida, tokens }.
131
+ */
132
+ function planejar({ todas, texto, modo, mensagens = [], nucleo, secoes, limite = 0, saidaMax = 8192, nivelMin = 0, jaAprendeu = new Set(), extraTokens = 0, aparar, sistemaFixo = null, forcar: forcarFora = [] }) {
133
+ const semLimite = !limite;
134
+ if (!semLimite) texto = recorte.pedidoReal(texto); // o gatilho olha o que a pessoa escreveu, não o embrulho do CLI
135
+ const forcar = [...forcarFora, ...(semLimite ? [] : recorte.forcarDoModo(modo))];
136
+ const { grupos, usados } = recorte.gruposDoPedido(texto, mensagens, forcar);
137
+ // o manual do Estúdio (seção do prompt) acompanha o modo, mesmo que a ferramenta de vídeo seja só a de motion
138
+ const opts = { sistemaFixo, forcar: semLimite ? forcar : [...forcar, ...usados, ...(recorte.forcarDoModo(modo).length ? ['estudio'] : [])], jaAprendeu };
139
+ const escolhidas = semLimite ? todas : recorte.escolher(todas, grupos);
140
+ const recortou = escolhidas.length < todas.length;
141
+ const comMais = recortou ? [...escolhidas, recorte.maisFerramentas(todas, grupos)] : escolhidas;
142
+ const restritas = recortou ? (() => { const g = recorte.principais(grupos); const e = recorte.escolher(todas, g); return [...e, recorte.maisFerramentas(todas, g)]; })() : comMais;
143
+
144
+ if (semLimite) {
145
+ const sistema = sistemaFixo || entrada.montarPrompt(nucleo, secoes, texto, opts);
146
+ return { tools: comMais, sistema, mensagens, nivel: 0, grupos, recortou: false, saida: saidaMax,
147
+ tokens: { ferramentas: tokensDe(comMais), sistema: tokensDe(sistema), conversa: tokensDe(mensagens), total: 0 } };
148
+ }
149
+
150
+ const saida = Math.min(saidaMax, Math.max(SAIDA_MINIMA, Math.floor(limite * FRACAO_SAIDA)));
151
+ let ultimo = null;
152
+ for (let nivel = Math.min(nivelMin, NIVEL_MAXIMO); nivel <= NIVEL_MAXIMO; nivel++) {
153
+ const tools = encurtar(nivel >= 5 ? restritas : comMais, TAMANHO_DESCRICAO[nivel]);
154
+ const sistema = sistemaDoNivel(nucleo, secoes, texto, opts, nivel, grupos);
155
+ const msgs = recorte.enxugarContexto(mensagens, nivel);
156
+ const conv = tokensDe(msgs);
157
+ const fixo = tokensDe(tools) + tokensDe(sistema) + extraTokens;
158
+ const sobra = Math.floor(limite * FRACAO_PEDIDO) - fixo - saida;
159
+ ultimo = { tools, sistema, nivel, fixo, sobra, msgs, conv };
160
+ if (sobra >= conv) break;
161
+ }
162
+ // Mesmo no fundo não coube a conversa: ela é que encolhe (a mais velha primeiro).
163
+ let msgs = ultimo.msgs;
164
+ if (ultimo.sobra < ultimo.conv && aparar) msgs = aparar(ultimo.msgs, Math.max(CONVERSA_MINIMA, ultimo.sobra));
165
+ const tokens = { ferramentas: tokensDe(ultimo.tools), sistema: tokensDe(ultimo.sistema), conversa: tokensDe(msgs) };
166
+ tokens.total = tokens.ferramentas + tokens.sistema + tokens.conversa + extraTokens;
167
+ return { tools: ultimo.tools, sistema: ultimo.sistema, mensagens: msgs, nivel: ultimo.nivel, grupos, recortou, saida, tokens, limite };
168
+ }
169
+
170
+ /* ── o que o provedor pediu de espera ────────────────────────────────────
171
+ * "7.66s", "1m2.5s", "150ms", "2m" — o formato dos headers x-ratelimit-reset-*. */
172
+ function duracaoMs(txt) {
173
+ const s = String(txt == null ? '' : txt).trim();
174
+ if (!s) return null;
175
+ if (/^\d+(\.\d+)?$/.test(s)) return Math.round(parseFloat(s) * 1000); // retry-after em segundos
176
+ let total = 0; let achou = false;
177
+ const re = /(\d+(?:\.\d+)?)(ms|h|m|s)/g;
178
+ let m;
179
+ while ((m = re.exec(s))) {
180
+ achou = true;
181
+ const v = parseFloat(m[1]);
182
+ total += m[2] === 'ms' ? v : m[2] === 's' ? v * 1000 : m[2] === 'm' ? v * 60000 : v * 3600000;
183
+ }
184
+ return achou ? Math.round(total) : null;
185
+ }
186
+
187
+ /** A frase simples para a pessoa — nunca o erro cru. */
188
+ function fraseDePausa(ms) {
189
+ const s = Math.max(1, Math.ceil(ms / 1000));
190
+ return `O modelo grátis pediu uma pausa de ${s}s. Já tento de novo com um pedido menor.`;
191
+ }
192
+
193
+ module.exports = { tokensDe, pisoDeNivel, subirPiso, LIMITES_GRATIS, NIVEL_MAXIMO, limiteTpm, aprender, esquecerAprendidos, planejar, encurtar, duracaoMs, fraseDePausa, ARQUIVO };
@@ -27,6 +27,7 @@
27
27
  const fs = require('fs');
28
28
  const os = require('os');
29
29
  const path = require('path');
30
+ const catalogo = require('../catalogo');
30
31
 
31
32
  const ARQUIVO = () => path.join(os.homedir(), '.primocode', 'provedor.json');
32
33
 
@@ -51,6 +52,8 @@ const PROVEDORES = {
51
52
  },
52
53
  gemini: {
53
54
  nome: 'Gemini (Google)',
55
+ env: ['GEMINI_API_KEY'],
56
+ noCatalogo: 'google',
54
57
  base: 'https://generativelanguage.googleapis.com/v1beta/openai',
55
58
  modelos: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'],
56
59
  onde: 'https://aistudio.google.com/apikey',
@@ -58,6 +61,8 @@ const PROVEDORES = {
58
61
  },
59
62
  chatgpt: {
60
63
  nome: 'ChatGPT (OpenAI)',
64
+ env: ['OPENAI_API_KEY'],
65
+ noCatalogo: 'openai',
61
66
  base: 'https://api.openai.com/v1',
62
67
  modelos: ['gpt-5-mini', 'gpt-5', 'gpt-4.1-mini', 'gpt-4o-mini'],
63
68
  onde: 'https://platform.openai.com/api-keys',
@@ -65,6 +70,8 @@ const PROVEDORES = {
65
70
  },
66
71
  claude: {
67
72
  nome: 'Claude (Anthropic)',
73
+ env: ['ANTHROPIC_API_KEY'],
74
+ noCatalogo: 'anthropic',
68
75
  base: 'https://api.anthropic.com/v1',
69
76
  modelos: ['claude-sonnet-5', 'claude-haiku-4-5-20251001', 'claude-opus-5-5'],
70
77
  onde: 'https://console.anthropic.com/settings/keys',
@@ -72,6 +79,7 @@ const PROVEDORES = {
72
79
  },
73
80
  groq: {
74
81
  nome: 'Groq',
82
+ env: ['GROQ_API_KEY'],
75
83
  base: 'https://api.groq.com/openai/v1',
76
84
  modelos: ['openai/gpt-oss-120b', 'llama-3.3-70b-versatile', 'openai/gpt-oss-20b'],
77
85
  onde: 'https://console.groq.com/keys',
@@ -79,6 +87,7 @@ const PROVEDORES = {
79
87
  },
80
88
  openrouter: {
81
89
  nome: 'OpenRouter',
90
+ env: ['OPENROUTER_API_KEY'],
82
91
  base: 'https://openrouter.ai/api/v1',
83
92
  modelos: ['openai/gpt-oss-120b', 'google/gemini-2.5-flash', 'anthropic/claude-sonnet-5'],
84
93
  onde: 'https://openrouter.ai/keys',
@@ -86,6 +95,7 @@ const PROVEDORES = {
86
95
  },
87
96
  deepseek: {
88
97
  nome: 'DeepSeek',
98
+ env: ['DEEPSEEK_API_KEY'],
89
99
  base: 'https://api.deepseek.com/v1',
90
100
  modelos: ['deepseek-chat'],
91
101
  onde: 'https://platform.deepseek.com/api_keys',
@@ -100,6 +110,66 @@ function mascarar(chave) {
100
110
  return `${k.slice(0, 4)}…${k.slice(-4)}`;
101
111
  }
102
112
 
113
+ /* ── DE ONDE VEM CADA PROVEDOR ────────────────────────────────────────────
114
+ * 1. os do menu (PROVEDORES acima) — curados, com o modelo preferido
115
+ * 2. os da pessoa — ~/.primocode/provedores.json, qualquer endereço que fale
116
+ * o formato da OpenAI (um gateway da empresa, um Ollama, um LiteLLM...):
117
+ * { "meu-gateway": { "nome": "Meu gateway", "base": "https://…/v1",
118
+ * "env": ["MEU_GATEWAY_KEY"], "modelos": ["gpt-x"] } }
119
+ * URL e chave moram NA MÁQUINA DA PESSOA, nunca no código.
120
+ * 3. os do catálogo models.dev (lib/provider/catalogo.js) — centenas
121
+ * A chave vem do arquivo do /api ou, se não houver, da variável de ambiente
122
+ * do provedor (GROQ_API_KEY etc.), como no OpenCode. */
123
+ const ARQUIVO_PROVEDORES = () => path.join(os.homedir(), '.primocode', 'provedores.json');
124
+
125
+ function provedoresDaPessoa() {
126
+ try {
127
+ const j = JSON.parse(fs.readFileSync(ARQUIVO_PROVEDORES(), 'utf8'));
128
+ const saida = {};
129
+ for (const [id, v] of Object.entries(j || {})) {
130
+ if (!v || typeof v.base !== 'string' || !/^https?:\/\//.test(v.base)) continue;
131
+ saida[id] = {
132
+ nome: String(v.nome || id), base: v.base.replace(/\/+$/, ''),
133
+ env: Array.isArray(v.env) ? v.env.map(String) : [],
134
+ modelos: Array.isArray(v.modelos) && v.modelos.length ? v.modelos.map(String) : [''],
135
+ dica: 'configurado por você', onde: '', propria: true,
136
+ };
137
+ }
138
+ return saida;
139
+ } catch { return {}; }
140
+ }
141
+
142
+ /** O provedor `id` de qualquer origem, ou null. */
143
+ function resolver(id) {
144
+ if (PROVEDORES[id]) return PROVEDORES[id];
145
+ const meus = provedoresDaPessoa();
146
+ if (meus[id]) return meus[id];
147
+ const c = catalogo.carregarSync().provedores[id];
148
+ if (!c) return null;
149
+ return {
150
+ nome: c.nome, base: c.api, env: c.env,
151
+ // quem usa ferramenta vem primeiro: o agente precisa delas
152
+ modelos: Object.keys(c.modelos).sort((a, b) => Number(c.modelos[b].ferr) - Number(c.modelos[a].ferr)),
153
+ dica: `${Object.keys(c.modelos).length} modelos (catálogo)`, onde: '', catalogo: true,
154
+ };
155
+ }
156
+
157
+ /** Os que a pessoa pode escolher, na ordem do menu: curados, os dela, o resto do catálogo. */
158
+ function todos() {
159
+ const lista = { ...PROVEDORES, ...provedoresDaPessoa() };
160
+ for (const id of Object.keys(catalogo.carregarSync().provedores)) if (!lista[id]) lista[id] = resolver(id);
161
+ return lista;
162
+ }
163
+
164
+ /** A chave de ambiente do provedor, se alguma das variáveis dele estiver preenchida. */
165
+ function chaveDoAmbiente(p) {
166
+ for (const nome of (p && p.env) || []) {
167
+ const v = process.env[nome];
168
+ if (v && v.trim()) return v.trim();
169
+ }
170
+ return '';
171
+ }
172
+
103
173
  let cache = null;
104
174
  let cacheMtime = 0;
105
175
 
@@ -108,20 +178,24 @@ function atual() {
108
178
  if (process.env.PRIMOCODE_SEM_PROVEDOR === '1') return null;
109
179
  let st;
110
180
  try { st = fs.statSync(ARQUIVO()); } catch { cache = null; return null; }
111
- if (cache && st.mtimeMs === cacheMtime) return cache;
112
- try {
113
- const j = JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8'));
114
- const p = PROVEDORES[j.provedor];
115
- cache = p && (j.chave || p.cli)
116
- ? { id: j.provedor, nome: p.nome, cli: !!p.cli, base: j.base || p.base, modelo: j.modelo || p.modelos[0], chave: j.chave || '' }
117
- : null;
118
- } catch { cache = null; }
119
- cacheMtime = st.mtimeMs;
120
- return cache;
181
+ if (!(cache && st.mtimeMs === cacheMtime)) {
182
+ try { cache = JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8')); } catch { cache = null; }
183
+ cacheMtime = st.mtimeMs;
184
+ }
185
+ if (!cache) return null;
186
+ const p = resolver(cache.provedor);
187
+ // a chave do ambiente é lida a cada chamada: trocar a variável vale na hora
188
+ const chave = cache.chave || chaveDoAmbiente(p);
189
+ if (!p || !(chave || p.cli)) return null;
190
+ return {
191
+ id: cache.provedor, nome: p.nome, cli: !!p.cli, base: cache.base || p.base,
192
+ modelo: cache.modelo || p.modelos[0], chave, daAmbiente: !cache.chave && !!chave,
193
+ noCatalogo: p.noCatalogo || (p.catalogo ? cache.provedor : (catalogo.carregarSync().provedores[cache.provedor] ? cache.provedor : '')),
194
+ };
121
195
  }
122
196
 
123
197
  function salvar({ provedor, chave, modelo }) {
124
- if (!PROVEDORES[provedor]) throw new Error(`provedor desconhecido: ${provedor}`);
198
+ if (!resolver(provedor)) throw new Error(`provedor desconhecido: ${provedor}`);
125
199
  const arq = ARQUIVO();
126
200
  fs.mkdirSync(path.dirname(arq), { recursive: true });
127
201
  fs.writeFileSync(arq, JSON.stringify({ provedor, chave: String(chave || '').trim(), modelo }, null, 2), { mode: 0o600 });
@@ -133,7 +207,45 @@ function salvar({ provedor, chave, modelo }) {
133
207
  function trocarModelo(modelo) {
134
208
  const a = atual();
135
209
  if (!a) return null;
136
- return salvar({ provedor: a.id, chave: a.chave, modelo });
210
+ return salvar({ provedor: a.id, chave: a.daAmbiente ? '' : a.chave, modelo });
211
+ }
212
+
213
+ /**
214
+ * Escolhe pelo nome: "modelo" (no provedor de agora) ou "provedor/modelo".
215
+ * OpenRouter tem ids com barra ("openai/gpt-oss-120b"): se o texto inteiro é
216
+ * um modelo do provedor de agora, é ele; só então a barra separa o provedor.
217
+ * Mudar de provedor exige a chave dele no ambiente (a do /api é de um só).
218
+ */
219
+ function escolherModelo(ref) {
220
+ const txt = String(ref || '').trim();
221
+ if (!txt) return { ok: false, erro: 'diga o modelo: /api modelo <modelo> ou <provedor>/<modelo>' };
222
+ const a = atual();
223
+ const pAtual = a && resolver(a.id);
224
+ // catálogo manda no que ele conhece; nos curados, modelo fora dele passa COM aviso (o catálogo pode estar atrás)
225
+ const noCat = (id, p) => { const k = p.noCatalogo || id; return catalogo.carregarSync().provedores[k] || null; };
226
+ const checar = (id, p, m) => {
227
+ const c = noCat(id, p);
228
+ if (p.catalogo) return p.modelos.includes(m) ? null : { erro: `${p.nome} não tem o modelo ${m} no catálogo. Veja: primocode modelos --provedor ${id}` };
229
+ if (c && !c.modelos[m]) return { aviso: `O catálogo não lista ${m} para ${p.nome}; se der 404, veja: primocode modelos --provedor ${p.noCatalogo || id}` };
230
+ return null;
231
+ };
232
+ if (a && (!txt.includes('/') || pAtual.modelos.includes(txt) || (noCat(a.id, pAtual) || { modelos: {} }).modelos[txt])) {
233
+ const v = checar(a.id, pAtual, txt);
234
+ if (v && v.erro) return { ok: false, erro: v.erro };
235
+ return { ok: true, atual: trocarModelo(txt), aviso: v && v.aviso };
236
+ }
237
+ const i = txt.indexOf('/');
238
+ const pid = i > 0 ? txt.slice(0, i) : '';
239
+ const mid = txt.slice(i + 1);
240
+ const p = pid && resolver(pid);
241
+ if (!p || !mid) return { ok: false, erro: `não achei "${txt}". Use <provedor>/<modelo> (primocode modelos lista) ou rode /api antes.` };
242
+ const v = checar(pid, p, mid);
243
+ if (v && v.erro) return { ok: false, erro: v.erro };
244
+ const guardada = a && a.id === pid && !a.daAmbiente ? a.chave : '';
245
+ if (!guardada && !chaveDoAmbiente(p) && !p.cli) {
246
+ return { ok: false, erro: `${p.nome} precisa de chave: defina ${(p.env || [])[0] || 'a chave'} no ambiente ou rode /api ${pid}.` };
247
+ }
248
+ return { ok: true, atual: salvar({ provedor: pid, chave: guardada, modelo: mid }), aviso: v && v.aviso };
137
249
  }
138
250
 
139
251
  function desligar() {
@@ -143,7 +255,8 @@ function desligar() {
143
255
 
144
256
  /** A chave colada serve? Pergunta a lista de modelos à própria API. */
145
257
  async function conferir(provedor, chave, { buscar = fetch } = {}) {
146
- const p = PROVEDORES[provedor];
258
+ const p = resolver(provedor);
259
+ if (!p) return { ok: false, error: `provedor desconhecido: ${provedor}` };
147
260
  let r;
148
261
  try {
149
262
  r = await buscar(`${p.base}/models`, {
@@ -166,4 +279,4 @@ async function conferir(provedor, chave, { buscar = fetch } = {}) {
166
279
  return { ok: true, modelo, disponiveis: lista };
167
280
  }
168
281
 
169
- module.exports = { PROVEDORES, ARQUIVO, mascarar, atual, salvar, trocarModelo, desligar, conferir };
282
+ module.exports = { PROVEDORES, ARQUIVO, ARQUIVO_PROVEDORES, mascarar, atual, salvar, trocarModelo, escolherModelo, desligar, conferir, resolver, todos, provedoresDaPessoa, chaveDoAmbiente };