pi-jev-guard 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -8
- package/config/jev-config.example.json +27 -4
- package/extensions/index.ts +219 -148
- package/package.json +1 -1
- package/src/automatic/overlay.ts +20 -1
- package/src/commands.ts +95 -13
- package/src/config.ts +68 -3
package/README.md
CHANGED
|
@@ -39,11 +39,20 @@ In chat, l'LLM può chiamare `jev_validate` per controlli mirati.
|
|
|
39
39
|
## Uso protetto
|
|
40
40
|
|
|
41
41
|
```text
|
|
42
|
-
/jev upstream deepseek deepseek-flash
|
|
43
|
-
/jev
|
|
42
|
+
/jev upstream deepseek deepseek-flash # crea deepseek/deepseek-flash__jev
|
|
43
|
+
/jev upstream openai-codex gpt-5.6-terra # aggiunge openai-codex/gpt-5.6-terra__jev
|
|
44
|
+
/jev twins # elenca attivi e salvati
|
|
45
|
+
/jev untwin openai-codex gpt-5.6-terra # rimuove uno solo
|
|
46
|
+
/jev mode automatic # seleziona il primario + attiva enforcement
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
**Più twin insieme**: ogni `/jev upstream` *aggiunge* un modello guarded, senza
|
|
50
|
+
rimuovere i precedenti. I twin vengono registrati anche all'avvio (i modelli
|
|
51
|
+
dinamici come Codex sono letti dal models-store), quindi la sessione riapre
|
|
52
|
+
direttamente sul `__jev` senza warning. Il **primario** (ultimo aggiunto, o
|
|
53
|
+
quello salvato come tale) è il target di auto-selezione e revert.
|
|
54
|
+
|
|
55
|
+
Nessun login extra: i twin vivono nello stesso provider e riusano la credenziale già configurata (env, stored, OAuth/SSO come Codex business plan). I modelli originali restano intatti e selezionabili.
|
|
47
56
|
|
|
48
57
|
In `automatic` il twin viene registrato già all'avvio (prima del ripristino del
|
|
49
58
|
modello di sessione), così la sessione può riaprire direttamente su `__jev`.
|
|
@@ -51,12 +60,13 @@ modello di sessione), così la sessione può riaprire direttamente su `__jev`.
|
|
|
51
60
|
Torna normale con `/model` (modello normale) + `/jev mode on-demand`.
|
|
52
61
|
In `automatic`, selezionare un modello non protetto lo fa ritornare
|
|
53
62
|
subito al twin (enforcement a livello selezione).
|
|
54
|
-
`/jev upstream` senza argomenti ripristina
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
`/jev upstream` senza argomenti ripristina tutti i twin salvati, `/jev untwin P M`
|
|
64
|
+
ne rimuove uno solo, `/jev off` li rimuove tutti
|
|
65
|
+
(e torna a on-demand, provider originali ripristinati). In `automatic` i twin
|
|
66
|
+
si ripristinano da soli al riavvio.
|
|
57
67
|
|
|
58
|
-
Se filtri i modelli con `enabledModels`, tieni
|
|
59
|
-
|
|
68
|
+
Se filtri i modelli con `enabledModels`, tieni le voci `__jev` dei twin che usi
|
|
69
|
+
o restano nascosti.
|
|
60
70
|
L'installazione punta alla cartella: dopo un aggiornamento del codice basta `/reload`.
|
|
61
71
|
|
|
62
72
|
Headless/CI: `JEV_AUTO_UPSTREAM=provider/model` crea il guarded all'avvio
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
"timeoutMs": 4000,
|
|
9
9
|
"timeoutMsTypesafe": 1500,
|
|
10
10
|
"timeoutMsOpenRouter": 4000,
|
|
11
|
-
"retryTransients": {
|
|
11
|
+
"retryTransients": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"maxRetries": 1
|
|
14
|
+
},
|
|
12
15
|
"apiKeyFile": "~/.pi/agent/jev-api-key.txt"
|
|
13
16
|
},
|
|
14
17
|
"policy": {
|
|
@@ -22,6 +25,16 @@
|
|
|
22
25
|
"transport": "provider-gate",
|
|
23
26
|
"maxRegenerations": 2,
|
|
24
27
|
"requireGuardedModel": true,
|
|
28
|
+
"twins": [
|
|
29
|
+
{
|
|
30
|
+
"provider": "deepseek",
|
|
31
|
+
"model": "deepseek-flash"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"provider": "openai-codex",
|
|
35
|
+
"model": "gpt-5.6-terra"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
25
38
|
"upstreamProvider": "deepseek",
|
|
26
39
|
"upstreamModel": "deepseek-flash"
|
|
27
40
|
},
|
|
@@ -44,7 +57,9 @@
|
|
|
44
57
|
},
|
|
45
58
|
"outputJudge": {
|
|
46
59
|
"enabled": true,
|
|
47
|
-
"tools": [
|
|
60
|
+
"tools": [
|
|
61
|
+
"bash"
|
|
62
|
+
],
|
|
48
63
|
"outputChars": 2000,
|
|
49
64
|
"leakThreshold": 0.9,
|
|
50
65
|
"minConfidence": 0.6,
|
|
@@ -52,8 +67,16 @@
|
|
|
52
67
|
},
|
|
53
68
|
"exfilCheck": {
|
|
54
69
|
"enabled": true,
|
|
55
|
-
"tools": [
|
|
56
|
-
|
|
70
|
+
"tools": [
|
|
71
|
+
"bash",
|
|
72
|
+
"powershell"
|
|
73
|
+
],
|
|
74
|
+
"blockOn": {
|
|
75
|
+
"destructive": 0.9,
|
|
76
|
+
"exfiltration": 0.7,
|
|
77
|
+
"beyondScope": 0.85,
|
|
78
|
+
"impact": 2.5
|
|
79
|
+
},
|
|
57
80
|
"minConfidence": 0.5,
|
|
58
81
|
"cacheSeconds": 120,
|
|
59
82
|
"blockWithoutUI": false
|
package/extensions/index.ts
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
parseUpstreamRef,
|
|
10
10
|
resolveBackend,
|
|
11
11
|
saveConfig,
|
|
12
|
+
twinRefs,
|
|
13
|
+
withTwinRefs,
|
|
12
14
|
type JevConfig,
|
|
15
|
+
type TwinRef,
|
|
13
16
|
} from "../src/config.ts";
|
|
14
17
|
import { createReviewer, createTypedAsk } from "../src/factory.ts";
|
|
15
18
|
import { createMetrics } from "../src/metrics.ts";
|
|
@@ -18,6 +21,7 @@ import {
|
|
|
18
21
|
STATIC_DEEPSEEK_FLASH,
|
|
19
22
|
buildOverlaidProvider,
|
|
20
23
|
isTwinId,
|
|
24
|
+
mergeModelsById,
|
|
21
25
|
twinIdFor,
|
|
22
26
|
} from "../src/automatic/overlay.ts";
|
|
23
27
|
import type { GateVerdictReport } from "../src/automatic/guardian.ts";
|
|
@@ -73,24 +77,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
73
77
|
let lastExfilErrorAt = 0;
|
|
74
78
|
const metrics = createMetrics();
|
|
75
79
|
|
|
76
|
-
//
|
|
77
|
-
// (o
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// Twin attivi, uno o più per provider. Nessun overlay finché la config non
|
|
81
|
+
// li attiva (automatic) o l'utente non usa /jev upstream (opt-in).
|
|
82
|
+
interface OverlayState {
|
|
83
|
+
upstream: Provider;
|
|
84
|
+
twinIds: string[];
|
|
85
|
+
}
|
|
86
|
+
const overlays = new Map<string, OverlayState>();
|
|
81
87
|
|
|
82
88
|
const getConfig = () => config;
|
|
83
89
|
const getReview = () => handle.review;
|
|
84
90
|
const getGateReview = () => gateHandle.review;
|
|
85
91
|
const getInfo = () => ({ backend: handle.backend, model: handle.model });
|
|
86
92
|
|
|
93
|
+
/** Twin primario (auto-selezione e revert): ultimo aggiunto che è attivo. */
|
|
94
|
+
function primaryTwin(): TwinRef | undefined {
|
|
95
|
+
const saved = twinRefs(config);
|
|
96
|
+
const wanted =
|
|
97
|
+
saved.find(
|
|
98
|
+
(r) =>
|
|
99
|
+
r.provider === config.automatic.upstreamProvider &&
|
|
100
|
+
r.model === config.automatic.upstreamModel,
|
|
101
|
+
) ?? saved[saved.length - 1];
|
|
102
|
+
if (wanted && overlays.get(wanted.provider)?.twinIds.includes(wanted.model)) {
|
|
103
|
+
return wanted;
|
|
104
|
+
}
|
|
105
|
+
// Ripiego: primo twin attivo.
|
|
106
|
+
for (const [providerId, state] of overlays) {
|
|
107
|
+
const model = state.twinIds[0];
|
|
108
|
+
if (model) return { provider: providerId, model };
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function activeTwinRefs(): TwinRef[] {
|
|
114
|
+
const refs: TwinRef[] = [];
|
|
115
|
+
for (const [providerId, state] of overlays) {
|
|
116
|
+
for (const model of state.twinIds) refs.push({ provider: providerId, model });
|
|
117
|
+
}
|
|
118
|
+
return refs;
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
function getGateStatus(): GateStatus {
|
|
88
|
-
|
|
122
|
+
const primary = primaryTwin();
|
|
123
|
+
if (primary) {
|
|
124
|
+
const count = activeTwinRefs().length;
|
|
89
125
|
return {
|
|
90
126
|
active: true,
|
|
91
|
-
twin: `${
|
|
92
|
-
upstream: `${
|
|
93
|
-
reason:
|
|
127
|
+
twin: `${primary.provider}/${twinIdFor(primary.model)}`,
|
|
128
|
+
upstream: `${primary.provider}/${primary.model}`,
|
|
129
|
+
reason: `${count} twin attiv${count === 1 ? "o" : "i"} su ${overlays.size} provider (stessa auth)`,
|
|
94
130
|
};
|
|
95
131
|
}
|
|
96
132
|
return {
|
|
@@ -99,13 +135,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
135
|
};
|
|
100
136
|
}
|
|
101
137
|
|
|
138
|
+
/** True se la coppia provider/modello è una twin da noi registrata. */
|
|
102
139
|
function isGuardedModel(provider: string | undefined, modelId: string | undefined): boolean {
|
|
103
|
-
if (!
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
isTwinId(modelId)
|
|
108
|
-
);
|
|
140
|
+
if (!provider || !modelId || !isTwinId(modelId)) return false;
|
|
141
|
+
const state = overlays.get(provider);
|
|
142
|
+
if (!state) return false;
|
|
143
|
+
return state.twinIds.some((model) => twinIdFor(model) === modelId);
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
function refresh() {
|
|
@@ -118,15 +153,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
153
|
return config;
|
|
119
154
|
}
|
|
120
155
|
|
|
121
|
-
/** Provider upstream vero
|
|
156
|
+
/** Provider upstream vero: il nostro upstream catturato, o il live dal registry. */
|
|
122
157
|
function liveUpstreamProvider(
|
|
123
158
|
registry: ModelRegistry,
|
|
124
159
|
providerId: string,
|
|
125
160
|
): Provider | undefined {
|
|
126
|
-
|
|
127
|
-
return upstreamRefs.get(providerId);
|
|
128
|
-
}
|
|
129
|
-
return registry.getProvider(providerId);
|
|
161
|
+
return overlays.get(providerId)?.upstream ?? registry.getProvider(providerId);
|
|
130
162
|
}
|
|
131
163
|
|
|
132
164
|
function snapshotUpstream(
|
|
@@ -147,56 +179,78 @@ export default function (pi: ExtensionAPI) {
|
|
|
147
179
|
return { provider, model: model as Model<Api> };
|
|
148
180
|
}
|
|
149
181
|
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
/** Registra (o aggiorna) l'overlay di un provider con i suoi twin. */
|
|
183
|
+
function registerOverlay(
|
|
184
|
+
providerId: string,
|
|
185
|
+
upstream: Provider,
|
|
186
|
+
twinIds: string[],
|
|
187
|
+
): void {
|
|
188
|
+
const overlay = buildOverlaidProvider({
|
|
189
|
+
upstream,
|
|
190
|
+
twinModelIds: twinIds,
|
|
191
|
+
getConfig,
|
|
192
|
+
getReview: getGateReview,
|
|
193
|
+
onVerdict: recordGateVerdict,
|
|
194
|
+
});
|
|
195
|
+
pi.registerProvider(overlay);
|
|
196
|
+
overlays.set(providerId, { upstream, twinIds: [...twinIds] });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Allinea il registry ai twin richiesti: registra i provider coinvolti e
|
|
201
|
+
* ripristina (unregister) quelli che non hanno più twin.
|
|
202
|
+
*/
|
|
203
|
+
function applyTwins(
|
|
204
|
+
refs: TwinRef[],
|
|
205
|
+
registry: ModelRegistry,
|
|
152
206
|
persist: boolean,
|
|
153
|
-
):
|
|
154
|
-
const
|
|
207
|
+
): { ok: boolean; error?: string; twins?: string[] } {
|
|
208
|
+
const byProvider = new Map<string, string[]>();
|
|
209
|
+
for (const ref of refs) {
|
|
210
|
+
const list = byProvider.get(ref.provider) ?? [];
|
|
211
|
+
if (!list.includes(ref.model)) list.push(ref.model);
|
|
212
|
+
byProvider.set(ref.provider, list);
|
|
213
|
+
}
|
|
214
|
+
|
|
155
215
|
try {
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
216
|
+
// Provider non più gemellati: unregister → builtin ripristinato.
|
|
217
|
+
for (const providerId of [...overlays.keys()]) {
|
|
218
|
+
if (!byProvider.has(providerId)) {
|
|
219
|
+
try {
|
|
220
|
+
pi.unregisterProvider(providerId);
|
|
221
|
+
} catch {
|
|
222
|
+
// ignora
|
|
223
|
+
}
|
|
224
|
+
overlays.delete(providerId);
|
|
162
225
|
}
|
|
163
|
-
upstreamRefs.delete(overlaidProviderId);
|
|
164
226
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
227
|
+
|
|
228
|
+
for (const [providerId, twinIds] of byProvider) {
|
|
229
|
+
// Cattura il live PRIMA dell'override (dopo, getProvider dà l'overlay).
|
|
230
|
+
const existing = overlays.get(providerId)?.upstream;
|
|
231
|
+
const live = existing ?? registry.getProvider(providerId);
|
|
232
|
+
if (!live) return { ok: false, error: `Provider non trovato: ${providerId}` };
|
|
233
|
+
for (const model of twinIds) {
|
|
234
|
+
const has = live.getModels().some((m) => m.id === model && !isTwinId(m.id));
|
|
235
|
+
if (!has) return { ok: false, error: `Modello non trovato: ${providerId}/${model}` };
|
|
236
|
+
}
|
|
237
|
+
registerOverlay(providerId, live, twinIds);
|
|
168
238
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
twinModelIds: [snapshot.model.id],
|
|
172
|
-
getConfig,
|
|
173
|
-
getReview: getGateReview,
|
|
174
|
-
onVerdict: recordGateVerdict,
|
|
175
|
-
});
|
|
176
|
-
pi.registerProvider(overlay);
|
|
177
|
-
overlaidProviderId = providerId;
|
|
178
|
-
twinTarget = { provider: providerId, model: snapshot.model.id };
|
|
179
|
-
config = {
|
|
180
|
-
...config,
|
|
181
|
-
automatic: {
|
|
182
|
-
...config.automatic,
|
|
183
|
-
upstreamProvider: providerId,
|
|
184
|
-
upstreamModel: snapshot.model.id,
|
|
185
|
-
},
|
|
186
|
-
};
|
|
239
|
+
|
|
240
|
+
config = withTwinRefs(config, refs);
|
|
187
241
|
if (persist) {
|
|
188
242
|
try {
|
|
189
243
|
saveConfig(config);
|
|
190
244
|
} catch (error) {
|
|
191
245
|
return {
|
|
192
246
|
ok: false,
|
|
193
|
-
error: `
|
|
247
|
+
error: `Twin aggiornati in memoria ma salvataggio fallito: ${
|
|
194
248
|
error instanceof Error ? error.message : String(error)
|
|
195
249
|
}`,
|
|
196
250
|
};
|
|
197
251
|
}
|
|
198
252
|
}
|
|
199
|
-
return { ok: true,
|
|
253
|
+
return { ok: true, twins: activeTwinRefs().map((r) => `${r.provider}/${twinIdFor(r.model)}`) };
|
|
200
254
|
} catch (error) {
|
|
201
255
|
return {
|
|
202
256
|
ok: false,
|
|
@@ -205,7 +259,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
259
|
}
|
|
206
260
|
}
|
|
207
261
|
|
|
208
|
-
/**
|
|
262
|
+
/** Aggiunge un twin (persistente). Gli altri restano attivi. */
|
|
209
263
|
function setUpstream(
|
|
210
264
|
registry: ModelRegistry,
|
|
211
265
|
providerId: string,
|
|
@@ -213,55 +267,78 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
267
|
): UpstreamResult {
|
|
214
268
|
const found = snapshotUpstream(registry, providerId, modelId);
|
|
215
269
|
if ("error" in found) return { ok: false, error: found.error };
|
|
216
|
-
|
|
270
|
+
const refs = twinRefs(config).filter(
|
|
271
|
+
(r) => !(r.provider === providerId && r.model === modelId),
|
|
272
|
+
);
|
|
273
|
+
refs.push({ provider: providerId, model: modelId });
|
|
274
|
+
const res = applyTwins(refs, registry, true);
|
|
275
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
276
|
+
return { ok: true, twin: `${providerId}/${twinIdFor(modelId)}` };
|
|
217
277
|
}
|
|
218
278
|
|
|
219
|
-
/**
|
|
279
|
+
/** Rimuove un twin; se il provider resta senza twin, ripristina il builtin. */
|
|
280
|
+
function removeTwin(
|
|
281
|
+
registry: ModelRegistry,
|
|
282
|
+
providerId: string,
|
|
283
|
+
modelId: string,
|
|
284
|
+
): { ok: boolean; error?: string } {
|
|
285
|
+
const refs = twinRefs(config);
|
|
286
|
+
const next = refs.filter((r) => !(r.provider === providerId && r.model === modelId));
|
|
287
|
+
if (next.length === refs.length) {
|
|
288
|
+
return { ok: false, error: `twin non registrato: ${providerId}/${modelId}` };
|
|
289
|
+
}
|
|
290
|
+
const res = applyTwins(next, registry, true);
|
|
291
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Riapplica tutti i twin salvati in config (es. dopo il riavvio). */
|
|
220
295
|
function refreshUpstream(registry: ModelRegistry): UpstreamResult {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
296
|
+
const res = applyTwins(twinRefs(config), registry, false);
|
|
297
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
298
|
+
const primary = primaryTwin();
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
twin: primary ? `${primary.provider}/${twinIdFor(primary.model)}` : undefined,
|
|
302
|
+
};
|
|
228
303
|
}
|
|
229
304
|
|
|
230
|
-
/** Rimuove
|
|
231
|
-
function removeGuarded(): { ok: boolean; error?: string } {
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
305
|
+
/** Rimuove tutti gli overlay (i builtin tornano da soli). */
|
|
306
|
+
function removeGuarded(): { ok: boolean; error?: string; removed?: number } {
|
|
307
|
+
if (overlays.size === 0) return { ok: false, error: "nessun guarded attivo" };
|
|
308
|
+
const removed = overlays.size;
|
|
309
|
+
for (const providerId of [...overlays.keys()]) {
|
|
310
|
+
try {
|
|
311
|
+
pi.unregisterProvider(providerId);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
error: error instanceof Error ? error.message : String(error),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
overlays.delete(providerId);
|
|
240
319
|
}
|
|
241
|
-
|
|
242
|
-
overlaidProviderId = undefined;
|
|
243
|
-
twinTarget = undefined;
|
|
244
|
-
return { ok: true };
|
|
320
|
+
return { ok: true, removed };
|
|
245
321
|
}
|
|
246
322
|
|
|
247
|
-
/** Seleziona il twin
|
|
323
|
+
/** Seleziona il twin primario come modello di sessione. */
|
|
248
324
|
async function switchToGuarded(
|
|
249
325
|
registry: ModelRegistry,
|
|
250
326
|
): Promise<{ ok: boolean; error?: string; model?: string }> {
|
|
251
|
-
|
|
327
|
+
const primary = primaryTwin();
|
|
328
|
+
if (!primary) {
|
|
252
329
|
return { ok: false, error: "nessun guarded attivo — prima /jev upstream <provider> <model>" };
|
|
253
330
|
}
|
|
254
|
-
const twinId = twinIdFor(
|
|
255
|
-
const model = registry.find(
|
|
331
|
+
const twinId = twinIdFor(primary.model);
|
|
332
|
+
const model = registry.find(primary.provider, twinId);
|
|
256
333
|
if (!model) {
|
|
257
|
-
return { ok: false, error: `twin non in registry: ${
|
|
334
|
+
return { ok: false, error: `twin non in registry: ${primary.provider}/${twinId}` };
|
|
258
335
|
}
|
|
259
336
|
try {
|
|
260
337
|
const ok = await pi.setModel(model);
|
|
261
338
|
if (!ok) {
|
|
262
|
-
return { ok: false, error: `auth mancante per ${
|
|
339
|
+
return { ok: false, error: `auth mancante per ${primary.provider} — /login ${primary.provider}` };
|
|
263
340
|
}
|
|
264
|
-
return { ok: true, model: `${
|
|
341
|
+
return { ok: true, model: `${primary.provider}/${model.id}` };
|
|
265
342
|
} catch (error) {
|
|
266
343
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
267
344
|
}
|
|
@@ -298,9 +375,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
298
375
|
reviewerInfo: getInfo,
|
|
299
376
|
metrics,
|
|
300
377
|
gateStatus: getGateStatus,
|
|
378
|
+
twinStatus: () => ({
|
|
379
|
+
active: activeTwinRefs(),
|
|
380
|
+
saved: twinRefs(config),
|
|
381
|
+
primary: primaryTwin(),
|
|
382
|
+
}),
|
|
301
383
|
setUpstream,
|
|
302
384
|
refreshUpstream,
|
|
303
385
|
removeGuarded,
|
|
386
|
+
removeTwin,
|
|
304
387
|
switchToGuarded,
|
|
305
388
|
getLastOutput: () => lastOutput,
|
|
306
389
|
getLastGate: () => lastGate,
|
|
@@ -314,59 +397,45 @@ export default function (pi: ExtensionAPI) {
|
|
|
314
397
|
// sostituisce col clone live dal registry.
|
|
315
398
|
const bootAuto = process.env.JEV_AUTO_UPSTREAM?.trim();
|
|
316
399
|
const autoOff = bootAuto === "0" || bootAuto?.toLowerCase() === "off";
|
|
317
|
-
let
|
|
400
|
+
let seedRefs: TwinRef[] = [];
|
|
318
401
|
if (bootAuto && !autoOff) {
|
|
319
402
|
const parsed = parseUpstreamRef([bootAuto]);
|
|
320
|
-
if (!("error" in parsed))
|
|
403
|
+
if (!("error" in parsed)) seedRefs = [parsed];
|
|
321
404
|
} else if (!bootAuto && config.mode === "automatic") {
|
|
322
|
-
|
|
323
|
-
provider: config.automatic.upstreamProvider,
|
|
324
|
-
model: config.automatic.upstreamModel,
|
|
325
|
-
};
|
|
405
|
+
seedRefs = twinRefs(config);
|
|
326
406
|
}
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
if (!
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
twinModelIds: [seedTarget.model],
|
|
360
|
-
getConfig,
|
|
361
|
-
getReview: getGateReview,
|
|
362
|
-
onVerdict: recordGateVerdict,
|
|
363
|
-
});
|
|
364
|
-
pi.registerProvider(overlay);
|
|
365
|
-
upstreamRefs.set(seedTarget.provider, seedUpstream);
|
|
366
|
-
overlaidProviderId = seedTarget.provider;
|
|
367
|
-
twinTarget = { provider: seedTarget.provider, model: seedTarget.model };
|
|
368
|
-
} catch {
|
|
369
|
-
// Nessun seed: il twin arriverà da session_start se possibile.
|
|
407
|
+
if (seedRefs.length > 0) {
|
|
408
|
+
// Raggruppa per provider: un overlay per provider con tutti i suoi twin.
|
|
409
|
+
const byProvider = new Map<string, string[]>();
|
|
410
|
+
for (const ref of seedRefs) {
|
|
411
|
+
const list = byProvider.get(ref.provider) ?? [];
|
|
412
|
+
if (!list.includes(ref.model)) list.push(ref.model);
|
|
413
|
+
byProvider.set(ref.provider, list);
|
|
414
|
+
}
|
|
415
|
+
for (const [providerId, twinIds] of byProvider) {
|
|
416
|
+
try {
|
|
417
|
+
// providers/all è mappato dagli alias di pi; il subpath del singolo
|
|
418
|
+
// provider non lo è e fallirebbe a load (solo installazioni npm).
|
|
419
|
+
const base = builtinProviders().find((p) => p.id === providerId);
|
|
420
|
+
if (!base) throw new Error(`provider ${providerId} not in builtins`);
|
|
421
|
+
// Il catalogo builtin può non avere i modelli salvati: provider
|
|
422
|
+
// dinamici (es. openai-codex) li tengono nel models-store, altri
|
|
423
|
+
// (deepseek-flash su versioni vecchie) in una definizione statica.
|
|
424
|
+
const extra = [
|
|
425
|
+
...(providerId === "deepseek" ? [STATIC_DEEPSEEK_FLASH] : []),
|
|
426
|
+
...readStoreModels(providerId),
|
|
427
|
+
];
|
|
428
|
+
const seedUpstream: Provider = {
|
|
429
|
+
...base,
|
|
430
|
+
getModels: () => mergeModelsById(base.getModels(), extra),
|
|
431
|
+
};
|
|
432
|
+
const known = new Set(seedUpstream.getModels().map((m) => m.id));
|
|
433
|
+
const usable = twinIds.filter((model) => known.has(model));
|
|
434
|
+
if (usable.length === 0) continue;
|
|
435
|
+
registerOverlay(providerId, seedUpstream, usable);
|
|
436
|
+
} catch {
|
|
437
|
+
// Provider non gemellabile all'avvio: session_start riproverà dal registry.
|
|
438
|
+
}
|
|
370
439
|
}
|
|
371
440
|
}
|
|
372
441
|
|
|
@@ -382,7 +451,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
382
451
|
if ("error" in found) {
|
|
383
452
|
if (ctx.hasUI) ctx.ui.notify(`JEV_AUTO_UPSTREAM fallito: ${found.error}`, "warning");
|
|
384
453
|
} else {
|
|
385
|
-
|
|
454
|
+
applyTwins([parsed], ctx.modelRegistry, false);
|
|
386
455
|
}
|
|
387
456
|
}
|
|
388
457
|
} catch {
|
|
@@ -392,7 +461,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
392
461
|
// Config automatic senza guarded = contraddittorio: ripristina + seleziona.
|
|
393
462
|
try {
|
|
394
463
|
if (config.mode !== "automatic" || !config.automatic.requireGuardedModel) return;
|
|
395
|
-
if (
|
|
464
|
+
if (overlays.size === 0) {
|
|
396
465
|
const restored = refreshUpstream(ctx.modelRegistry);
|
|
397
466
|
if (!restored.ok) {
|
|
398
467
|
if (ctx.hasUI) {
|
|
@@ -401,17 +470,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
401
470
|
return;
|
|
402
471
|
}
|
|
403
472
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
473
|
+
const primary = primaryTwin();
|
|
474
|
+
if (!primary) return;
|
|
475
|
+
const twinId = twinIdFor(primary.model);
|
|
476
|
+
if (ctx.model?.provider === primary.provider && ctx.model?.id === twinId) return;
|
|
477
|
+
const twin = ctx.modelRegistry.find(primary.provider, twinId);
|
|
408
478
|
if (!twin) return;
|
|
409
479
|
const switched = await pi.setModel(twin);
|
|
410
480
|
if (ctx.hasUI) {
|
|
411
481
|
ctx.ui.notify(
|
|
412
482
|
switched
|
|
413
|
-
? `Jev automatic: selezionato ${
|
|
414
|
-
: `Jev automatic: auth mancante per ${
|
|
483
|
+
? `Jev automatic: selezionato ${primary.provider}/${twinId}.`
|
|
484
|
+
: `Jev automatic: auth mancante per ${primary.provider} — /login ${primary.provider}.`,
|
|
415
485
|
switched ? "info" : "warning",
|
|
416
486
|
);
|
|
417
487
|
}
|
|
@@ -425,13 +495,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
425
495
|
pi.on("model_select", async (event, ctx) => {
|
|
426
496
|
try {
|
|
427
497
|
if (config.mode !== "automatic" || !config.automatic.requireGuardedModel) return;
|
|
428
|
-
if (
|
|
498
|
+
if (overlays.size === 0) return;
|
|
429
499
|
if (isGuardedModel(event.model.provider, event.model.id)) return;
|
|
430
500
|
|
|
431
|
-
const
|
|
432
|
-
const twin = ctx.modelRegistry.find(overlaidProviderId, twinId);
|
|
501
|
+
const primary = primaryTwin();
|
|
433
502
|
const fallback =
|
|
434
|
-
|
|
503
|
+
(primary ? ctx.modelRegistry.find(primary.provider, twinIdFor(primary.model)) : undefined) ??
|
|
435
504
|
(event.previousModel && isGuardedModel(event.previousModel.provider, event.previousModel.id)
|
|
436
505
|
? event.previousModel
|
|
437
506
|
: undefined);
|
|
@@ -671,9 +740,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
671
740
|
|
|
672
741
|
const msg = event.message as { provider?: string; model?: string };
|
|
673
742
|
if (isGuardedModel(msg.provider, msg.model)) {
|
|
743
|
+
const primary = primaryTwin();
|
|
674
744
|
pi.appendEntry("jev-guarded", {
|
|
675
|
-
twin:
|
|
676
|
-
upstream:
|
|
745
|
+
twin: msg.model ? `${msg.provider}/${msg.model}` : undefined,
|
|
746
|
+
upstream: primary ? `${primary.provider}/${primary.model}` : undefined,
|
|
747
|
+
active: activeTwinRefs().map((r) => `${r.provider}/${r.model}`),
|
|
677
748
|
});
|
|
678
749
|
return;
|
|
679
750
|
}
|
package/package.json
CHANGED
package/src/automatic/overlay.ts
CHANGED
|
@@ -100,6 +100,21 @@ export function twinModelFor(
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/** Unione di liste modelli con dedup per id (il primo vince). */
|
|
104
|
+
export function mergeModelsById(
|
|
105
|
+
primary: readonly Model<Api>[],
|
|
106
|
+
extra: readonly Model<Api>[],
|
|
107
|
+
): Model<Api>[] {
|
|
108
|
+
const out: Model<Api>[] = [];
|
|
109
|
+
const seen = new Set<string>();
|
|
110
|
+
for (const model of [...primary, ...extra]) {
|
|
111
|
+
if (!model || typeof model.id !== "string" || seen.has(model.id)) continue;
|
|
112
|
+
seen.add(model.id);
|
|
113
|
+
out.push(model);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
function stripTwins(models: readonly Model<Api>[]): Model<Api>[] {
|
|
104
119
|
return models.filter((m) => !isTwinId(m.id)).map((m) => ({ ...m }));
|
|
105
120
|
}
|
|
@@ -114,13 +129,17 @@ export function buildOverlaidProvider(deps: OverlayDeps): Provider {
|
|
|
114
129
|
let twinToOriginal = new Map<string, Model<Api>>();
|
|
115
130
|
let currentModels: Model<Api>[] = [];
|
|
116
131
|
|
|
117
|
-
function rebuildTwins(
|
|
132
|
+
function rebuildTwins(rawOriginals: readonly Model<Api>[]): Model<Api>[] {
|
|
118
133
|
twinToOriginal = new Map();
|
|
119
134
|
const chosen = new Set(deps.twinModelIds);
|
|
135
|
+
// Dedup per id: sorgenti miste (builtin + store + statica) possono
|
|
136
|
+
// ripetere lo stesso modello e produrrebbero twin duplicati.
|
|
137
|
+
const originals = mergeModelsById(rawOriginals, []);
|
|
120
138
|
const twins: Model<Api>[] = [];
|
|
121
139
|
for (const original of originals) {
|
|
122
140
|
if (chosen.has(original.id)) {
|
|
123
141
|
const twin = twinModelFor(original, upstream.id);
|
|
142
|
+
if (twinToOriginal.has(twin.id)) continue;
|
|
124
143
|
twinToOriginal.set(twin.id, original);
|
|
125
144
|
twins.push(twin);
|
|
126
145
|
}
|
package/src/commands.ts
CHANGED
|
@@ -2,8 +2,15 @@ import { readFileSync, existsSync, statSync, realpathSync, writeFileSync, mkdirS
|
|
|
2
2
|
import { resolve, dirname } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import type { ExtensionAPI, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { JevConfig } from "./config.ts";
|
|
6
|
-
import {
|
|
5
|
+
import type { JevConfig, TwinRef } from "./config.ts";
|
|
6
|
+
import {
|
|
7
|
+
defaultConfigPath,
|
|
8
|
+
expandHome,
|
|
9
|
+
parseUpstreamRef,
|
|
10
|
+
resolveBackend,
|
|
11
|
+
saveConfig,
|
|
12
|
+
twinRefs,
|
|
13
|
+
} from "./config.ts";
|
|
7
14
|
import type { ReviewFn } from "./reviewer.ts";
|
|
8
15
|
import { reviewWithCoverage } from "./reviewer.ts";
|
|
9
16
|
import type { Metrics } from "./metrics.ts";
|
|
@@ -17,6 +24,15 @@ export interface GateStatusView {
|
|
|
17
24
|
reason: string;
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
export interface TwinStatusView {
|
|
28
|
+
/** Twin attivi adesso (overlay registrati). */
|
|
29
|
+
active: TwinRef[];
|
|
30
|
+
/** Twin salvati in config (ripristinati all'avvio/refresh). */
|
|
31
|
+
saved: TwinRef[];
|
|
32
|
+
/** Twin primario: auto-selezione e revert. */
|
|
33
|
+
primary?: TwinRef;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
export interface UpstreamResult {
|
|
21
37
|
ok: boolean;
|
|
22
38
|
error?: string;
|
|
@@ -37,7 +53,13 @@ export interface JevCommandDeps {
|
|
|
37
53
|
modelId: string,
|
|
38
54
|
) => UpstreamResult;
|
|
39
55
|
refreshUpstream: (registry: ModelRegistry) => UpstreamResult;
|
|
40
|
-
removeGuarded: () => { ok: boolean; error?: string };
|
|
56
|
+
removeGuarded: () => { ok: boolean; error?: string; removed?: number };
|
|
57
|
+
removeTwin: (
|
|
58
|
+
registry: ModelRegistry,
|
|
59
|
+
providerId: string,
|
|
60
|
+
modelId: string,
|
|
61
|
+
) => { ok: boolean; error?: string };
|
|
62
|
+
twinStatus: () => TwinStatusView;
|
|
41
63
|
switchToGuarded: (registry: ModelRegistry) => Promise<{
|
|
42
64
|
ok: boolean;
|
|
43
65
|
error?: string;
|
|
@@ -52,6 +74,8 @@ export const JEV_SUBCOMMANDS = [
|
|
|
52
74
|
"mode",
|
|
53
75
|
"models",
|
|
54
76
|
"upstream",
|
|
77
|
+
"twins",
|
|
78
|
+
"untwin",
|
|
55
79
|
"off",
|
|
56
80
|
"refresh-upstream",
|
|
57
81
|
"check",
|
|
@@ -70,10 +94,12 @@ export const JEV_HELP_TEXT = [
|
|
|
70
94
|
" /jev mode on-demand Solo verifiche esplicite (default, zero costi impliciti)",
|
|
71
95
|
" /jev mode automatic Enforcement: seleziona il twin guarded + verifica",
|
|
72
96
|
" /jev models [filtro] Modelli pi (● twin guarded attivo, ★ ultimo salvato)",
|
|
73
|
-
" /jev upstream P M
|
|
97
|
+
" /jev upstream P M Aggiunge il twin M__jev nel provider P (salvato)",
|
|
74
98
|
" /jev upstream P/M Stessa cosa in forma compatta",
|
|
75
|
-
" /jev upstream
|
|
76
|
-
" /jev
|
|
99
|
+
" /jev upstream Ripristina tutti i twin salvati",
|
|
100
|
+
" /jev twins Elenca twin attivi e salvati (● attivo, ★ primario)",
|
|
101
|
+
" /jev untwin P M Rimuove un twin (anche P/M)",
|
|
102
|
+
" /jev off Rimuove tutti i twin (torna on-demand)",
|
|
77
103
|
" /jev refresh-upstream Ricrea il twin dal registry (dopo refresh cataloghi)",
|
|
78
104
|
" /jev check <file> [req] Verifica un file subito, senza cambiare modalità",
|
|
79
105
|
" /jev stats Conteggi pass/block/review/unavailable + latenze",
|
|
@@ -114,7 +140,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
114
140
|
const resolved = resolveBackend(cfg);
|
|
115
141
|
const info = deps.reviewerInfo();
|
|
116
142
|
const gate = deps.gateStatus();
|
|
117
|
-
const
|
|
143
|
+
const twinView = deps.twinStatus();
|
|
144
|
+
const authProvider = twinView.primary?.provider ?? cfg.automatic.upstreamProvider;
|
|
118
145
|
let guardedAuth = "—";
|
|
119
146
|
if (gate.active) {
|
|
120
147
|
try {
|
|
@@ -165,7 +192,8 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
165
192
|
`Verificatore timeout: ${resolved.timeoutMs}ms`,
|
|
166
193
|
`Policy: ${cfg.policy.revision} pass<=${cfg.policy.passMaxFlawProbability} block>=${cfg.policy.blockMinFlawProbability}`,
|
|
167
194
|
`Chiave presente: ${resolved.apiKeyPresent ? "sì" : "no"} (${resolved.keySource})`,
|
|
168
|
-
`Gate: ${gate.active ? `attivo (twin ${gate.twin}
|
|
195
|
+
`Gate: ${gate.active ? `attivo (twin ${gate.twin})` : `non attivo (${gate.reason})`}`,
|
|
196
|
+
`Twin attivi: ${twinView.active.length > 0 ? twinView.active.map((r) => `${r.provider}/${r.model}__jev`).join(", ") : "nessuno"}`,
|
|
169
197
|
`Twin nel picker: ${twinScope}`,
|
|
170
198
|
`Modello corrente: ${currentModel}`,
|
|
171
199
|
`Ultimo upstream salvato: ${cfg.automatic.upstreamProvider}/${cfg.automatic.upstreamModel}`,
|
|
@@ -242,12 +270,13 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
242
270
|
}
|
|
243
271
|
|
|
244
272
|
if (sub === "upstream") {
|
|
245
|
-
// Senza argomenti:
|
|
273
|
+
// Senza argomenti: ripristina tutti i twin salvati.
|
|
246
274
|
if (parts.length === 1) {
|
|
247
275
|
const res = deps.refreshUpstream(ctx.modelRegistry);
|
|
276
|
+
const count = deps.twinStatus().active.length;
|
|
248
277
|
ctx.ui.notify(
|
|
249
278
|
res.ok
|
|
250
|
-
? `Twin
|
|
279
|
+
? `Twin ripristinati: ${count} (primario ${res.twin ?? "—"}). Seleziona con /model.`
|
|
251
280
|
: `Ripristino fallito: ${res.error}`,
|
|
252
281
|
res.ok ? "info" : "error",
|
|
253
282
|
);
|
|
@@ -260,17 +289,70 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
260
289
|
}
|
|
261
290
|
const res = deps.setUpstream(ctx.modelRegistry, parsed.provider, parsed.model);
|
|
262
291
|
if (!res.ok) {
|
|
263
|
-
ctx.ui.notify(`
|
|
292
|
+
ctx.ui.notify(`Twin non aggiunto: ${res.error}`, "error");
|
|
264
293
|
return;
|
|
265
294
|
}
|
|
295
|
+
const total = deps.twinStatus().active.length;
|
|
266
296
|
ctx.ui.notify(
|
|
267
|
-
`Twin
|
|
297
|
+
`Twin aggiunto: ${res.twin} (totale attivi: ${total}, stessa auth del provider). ` +
|
|
268
298
|
`Selezionalo con /model per enforcement in automatic.`,
|
|
269
299
|
"info",
|
|
270
300
|
);
|
|
271
301
|
return;
|
|
272
302
|
}
|
|
273
303
|
|
|
304
|
+
if (sub === "twins") {
|
|
305
|
+
const view = deps.twinStatus();
|
|
306
|
+
if (view.active.length === 0 && view.saved.length === 0) {
|
|
307
|
+
ctx.ui.notify(
|
|
308
|
+
"Jev: nessun twin — aggiungine con /jev upstream <provider> <model>.",
|
|
309
|
+
"info",
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const primaryKey = view.primary
|
|
314
|
+
? `${view.primary.provider}/${view.primary.model}`
|
|
315
|
+
: undefined;
|
|
316
|
+
const activeKeys = new Set(view.active.map((r) => `${r.provider}/${r.model}`));
|
|
317
|
+
const lines: string[] = [];
|
|
318
|
+
for (const ref of view.saved) {
|
|
319
|
+
const key = `${ref.provider}/${ref.model}`;
|
|
320
|
+
const mark = activeKeys.has(key) ? "● " : "○ ";
|
|
321
|
+
const star = key === primaryKey ? "★" : " ";
|
|
322
|
+
lines.push(`${mark}${star} ${key}${activeKeys.has(key) ? "" : " (non attivo)"}`);
|
|
323
|
+
}
|
|
324
|
+
ctx.ui.notify(
|
|
325
|
+
[
|
|
326
|
+
`Twin attivi: ${view.active.length} · salvati: ${view.saved.length}`,
|
|
327
|
+
"● registrato ★ primario (auto-selezione/revert)",
|
|
328
|
+
"",
|
|
329
|
+
...lines,
|
|
330
|
+
].join("\n"),
|
|
331
|
+
"info",
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (sub === "untwin") {
|
|
337
|
+
const parsed = parseUpstreamRef(parts.slice(1));
|
|
338
|
+
if ("error" in parsed) {
|
|
339
|
+
ctx.ui.notify(
|
|
340
|
+
`${parsed.error.replace("/jev upstream", "/jev untwin")}`,
|
|
341
|
+
"warning",
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const res = deps.removeTwin(ctx.modelRegistry, parsed.provider, parsed.model);
|
|
346
|
+
const total = deps.twinStatus().active.length;
|
|
347
|
+
ctx.ui.notify(
|
|
348
|
+
res.ok
|
|
349
|
+
? `Twin rimosso: ${parsed.provider}/${parsed.model} (attivi: ${total}).`
|
|
350
|
+
: `Niente da rimuovere: ${res.error}`,
|
|
351
|
+
res.ok ? "info" : "warning",
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
274
356
|
if (sub === "off") {
|
|
275
357
|
const res = deps.removeGuarded();
|
|
276
358
|
if (res.ok) {
|
|
@@ -279,7 +361,7 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
|
|
|
279
361
|
}
|
|
280
362
|
ctx.ui.notify(
|
|
281
363
|
res.ok
|
|
282
|
-
?
|
|
364
|
+
? `Twin rimossi: ${res.removed ?? 0} (provider originali ripristinati), modalità on-demand.`
|
|
283
365
|
: `Niente da rimuovere: ${res.error}`,
|
|
284
366
|
res.ok ? "info" : "warning",
|
|
285
367
|
);
|
package/src/config.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface JevConfig {
|
|
|
32
32
|
transport: "provider-gate";
|
|
33
33
|
maxRegenerations: number;
|
|
34
34
|
requireGuardedModel: boolean;
|
|
35
|
+
/** Twin salvati: più modelli guarded insieme (uno o più per provider). */
|
|
36
|
+
twins: TwinRef[];
|
|
37
|
+
/** Ultimo twin aggiunto: primario per auto-selezione e revert. */
|
|
35
38
|
upstreamProvider: string;
|
|
36
39
|
upstreamModel: string;
|
|
37
40
|
};
|
|
@@ -89,6 +92,11 @@ export interface JevConfig {
|
|
|
89
92
|
};
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
export interface TwinRef {
|
|
96
|
+
provider: string;
|
|
97
|
+
model: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
export const DEFAULT_CONFIG: JevConfig = {
|
|
93
101
|
version: 1,
|
|
94
102
|
mode: "on-demand",
|
|
@@ -114,8 +122,11 @@ export const DEFAULT_CONFIG: JevConfig = {
|
|
|
114
122
|
transport: "provider-gate",
|
|
115
123
|
maxRegenerations: 2,
|
|
116
124
|
requireGuardedModel: true,
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
twins: [],
|
|
126
|
+
// Vuoti di default: nessun twin "salvato" finché l'utente non ne aggiunge
|
|
127
|
+
// uno con /jev upstream. Il seed usa questa lista (o l'env).
|
|
128
|
+
upstreamProvider: "",
|
|
129
|
+
upstreamModel: "",
|
|
119
130
|
},
|
|
120
131
|
limits: {
|
|
121
132
|
maxPayloadBytes: 120000,
|
|
@@ -184,11 +195,15 @@ export function loadConfig(configPath?: string): JevConfig {
|
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
const
|
|
198
|
+
const loaded = mergeDeep(
|
|
188
199
|
JSON.parse(JSON.stringify(DEFAULT_CONFIG)),
|
|
189
200
|
fileConfig,
|
|
190
201
|
) as JevConfig;
|
|
191
202
|
|
|
203
|
+
// Normalizza la lista twin (config scritta a mano o da versioni vecchie):
|
|
204
|
+
// dedup, scarta voci malformate, garantisce che il primario sia incluso.
|
|
205
|
+
let merged = withTwinRefs(loaded, twinRefs(loaded));
|
|
206
|
+
|
|
192
207
|
const envMode = process.env.JEV_MODE;
|
|
193
208
|
if (envMode === "on-demand" || envMode === "automatic") {
|
|
194
209
|
merged.mode = envMode;
|
|
@@ -352,6 +367,56 @@ export function configSnapshot(config: JevConfig): JevConfig {
|
|
|
352
367
|
return JSON.parse(JSON.stringify(config)) as JevConfig;
|
|
353
368
|
}
|
|
354
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Twin salvati: la lista `automatic.twins` se popolata, altrimenti il singolo
|
|
372
|
+
* upstream primario (migrazione dalle versioni precedenti). Semplifica e
|
|
373
|
+
* scarta voci malformate: la config è modificabile a mano.
|
|
374
|
+
*/
|
|
375
|
+
export function twinRefs(config: JevConfig): TwinRef[] {
|
|
376
|
+
const raw = Array.isArray(config.automatic.twins) ? config.automatic.twins : [];
|
|
377
|
+
const cleaned: TwinRef[] = [];
|
|
378
|
+
const seen = new Set<string>();
|
|
379
|
+
for (const entry of raw) {
|
|
380
|
+
if (!entry || typeof entry.provider !== "string" || typeof entry.model !== "string") continue;
|
|
381
|
+
const provider = entry.provider.trim();
|
|
382
|
+
const model = entry.model.trim();
|
|
383
|
+
if (!provider || !model) continue;
|
|
384
|
+
const key = `${provider}\0${model}`;
|
|
385
|
+
if (seen.has(key)) continue;
|
|
386
|
+
seen.add(key);
|
|
387
|
+
cleaned.push({ provider, model });
|
|
388
|
+
}
|
|
389
|
+
if (cleaned.length > 0) return cleaned;
|
|
390
|
+
const provider = config.automatic.upstreamProvider?.trim();
|
|
391
|
+
const model = config.automatic.upstreamModel?.trim();
|
|
392
|
+
return provider && model ? [{ provider, model }] : [];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Applica una lista di twin alla config: la lista è la fonte di verità.
|
|
397
|
+
* Il campo primario (auto-selezione/revert) resta se già presente nella
|
|
398
|
+
* lista, altrimenti diventa l'ultimo aggiunto.
|
|
399
|
+
*/
|
|
400
|
+
export function withTwinRefs(config: JevConfig, refs: TwinRef[]): JevConfig {
|
|
401
|
+
const primary = refs.find(
|
|
402
|
+
(r) =>
|
|
403
|
+
r.provider === config.automatic.upstreamProvider &&
|
|
404
|
+
r.model === config.automatic.upstreamModel,
|
|
405
|
+
) ?? refs[refs.length - 1];
|
|
406
|
+
return {
|
|
407
|
+
...config,
|
|
408
|
+
automatic: {
|
|
409
|
+
...config.automatic,
|
|
410
|
+
twins: refs.map((r) => ({ ...r })),
|
|
411
|
+
...(primary ? { upstreamProvider: primary.provider, upstreamModel: primary.model } : {}),
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function twinKey(ref: TwinRef): string {
|
|
417
|
+
return `${ref.provider}/${ref.model}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
355
420
|
export function saveConfig(config: JevConfig, configPath?: string): string {
|
|
356
421
|
const path = configPath ?? defaultConfigPath();
|
|
357
422
|
mkdirSync(dirname(path), { recursive: true });
|