primocode 11.3.3 → 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.
- package/LICENSES/edit2-terceiros.md +31 -0
- package/LICENSES/models-dev-MIT.txt +25 -0
- package/README.md +2 -2
- package/editor/SKILL.md +24 -0
- package/editor/THIRD_PARTY_NOTICES.md +1 -0
- package/editor/assets/baixar.json +9 -1
- package/editor/helpers/df_limpar.py +29 -0
- package/editor/helpers/edit2.py +155 -0
- package/editor/helpers/enquadrar_camera.py +77 -0
- package/editor/helpers/face_layout.py +20 -0
- package/editor/helpers/face_mediapipe.py +147 -0
- package/editor/helpers/instalar_df.sh +7 -0
- package/editor/helpers/legenda_ass.py +124 -0
- package/editor/helpers/look_ffmpeg.py +105 -0
- package/editor/helpers/motor_palavras.py +157 -0
- package/editor/helpers/primeiro_passe.py +214 -0
- package/editor/helpers/som_edit2.py +119 -0
- package/editor/helpers/transcribe.py +31 -2
- package/editor/pyproject.toml +9 -0
- package/editor/references/edit2.md +22 -0
- package/editor/uv.lock +3342 -214
- package/kits/3d/LICENSES/README.md +11 -0
- package/kits/3d/animacoes.js +87 -0
- package/kits/3d/base.js +44 -0
- package/kits/3d/catalogo.js +38 -0
- package/kits/3d/cenario.js +162 -0
- package/kits/3d/exemplos/asfalto.mjs +91 -0
- package/kits/3d/maquinas.js +97 -0
- package/kits/3d/montador.js +142 -0
- package/kits/3d/package.json +1 -0
- package/kits/3d/personagem.js +86 -0
- package/kits/3d/remotion/Maquete.jsx +42 -0
- package/kits/3d/remotion/Root.jsx +10 -0
- package/kits/3d/render.mjs +73 -0
- package/lib/cli/comando-api.js +30 -8
- package/lib/cli/modelos-lista.js +58 -0
- package/lib/cli/sessao.js +11 -0
- package/lib/cli/subcomandos.js +3 -2
- package/lib/outra-pasta.js +1 -1
- package/lib/permissao-sistema.js +60 -0
- package/lib/permission/perigo.js +2 -1
- package/lib/permission/permissao.js +1 -0
- package/lib/permission/sem-perguntar.js +29 -0
- package/lib/plugin/desktop.js +5 -4
- package/lib/plugin/estudio/cota.js +4 -2
- package/lib/plugin/estudio/dialogo3d.js +71 -0
- package/lib/plugin/estudio/effectcraft.js +1 -1
- package/lib/plugin/estudio/glifos.js +2 -0
- package/lib/plugin/estudio/gsap-local.js +43 -0
- package/lib/plugin/estudio/index.js +63 -10
- package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
- package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
- package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
- package/lib/plugin/estudio/maquina-diretor.js +1 -1
- package/lib/plugin/estudio/maquina-elementos.js +2 -2
- package/lib/plugin/estudio/maquina.js +26 -8
- package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
- package/lib/plugin/estudio/motion-plano/critico.js +59 -0
- package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
- package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
- package/lib/plugin/estudio/motion-plano/index.js +75 -0
- package/lib/plugin/estudio/motion-plano/llm.js +59 -0
- package/lib/plugin/estudio/motion-plano/motor.js +50 -0
- package/lib/plugin/estudio/motion-plano/passos.js +38 -0
- package/lib/plugin/estudio/motion-plano/pele.js +36 -0
- package/lib/plugin/estudio/motion-plano/plano.js +152 -0
- package/lib/plugin/estudio/motion-plano/regua.js +100 -0
- package/lib/plugin/estudio/personagem.js +3 -1
- package/lib/plugin/estudio/referencias.js +1 -1
- package/lib/provider/catalogo.js +161 -0
- package/lib/provider/local/catalogo-embutido.json.gz +0 -0
- package/lib/provider/local/cerebro.json +1 -1
- package/lib/provider/local/entrada.js +10 -0
- package/lib/provider/local/motor.js +140 -27
- package/lib/provider/local/orcamento.js +193 -0
- package/lib/provider/local/provedor.js +127 -14
- package/lib/provider/local/recorte.js +170 -0
- package/lib/provider/local/revezamento.js +121 -0
- package/lib/session/runner/act.js +28 -3
- package/lib/skill/catalogo.js +9 -1
- package/lib/tool/reparar-entrada.js +1 -0
- package/lib/tool/tools.js +58 -1
- package/lib/tui/ui.js +1 -1
- package/package.json +2 -2
- package/vendor/fontes/ttf/ORIGEM.txt +1 -0
- package/vendor/fontes/ttf/inter-800.ttf +0 -0
- package/vendor/gsap/ORIGEM.txt +6 -0
- package/vendor/gsap/gsap.min.js +11 -0
- package/voz/primo_voz/chat.html +206 -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
|
-
|
|
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
|
|
76
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
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 (!
|
|
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 =
|
|
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 };
|