pi-jev-guard 0.2.2 → 0.3.1

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 CHANGED
@@ -54,6 +54,29 @@ In chat, l'LLM può chiamare `jev_validate` per controlli mirati.
54
54
  /jev mode automatic # seleziona il primario + attiva enforcement
55
55
  ```
56
56
 
57
+ ### Backend e chiavi
58
+
59
+ Due backend, stessa policy e stesse regole:
60
+
61
+ | Backend | Endpoint | Variabile | Modello | Timeout |
62
+ |---|---|---|---|---|
63
+ | `openrouter` (default) | OpenRouter | `OPENROUTER_API_KEY` | `typesafe/jev-1.13` | 4000 ms |
64
+ | `typesafe` | SDK ufficiale `@typesafe-ai/sdk` → `api.typesafe.ai` | `TYPESAFE_API_KEY` | `jev-1.13.0` | 1500 ms |
65
+ | `auto` | sceglie il backend che ha una chiave (entrambe → OpenRouter) | — | — | — |
66
+
67
+ ```text
68
+ /jev backend # stato: backend, modello, chiavi rilevate
69
+ /jev backend typesafe # cambia backend (salvato in jev-config.json)
70
+ /jev save-key typesafe # salva la chiave del backend (file 600)
71
+ /jev save-key openrouter # l'altra chiave resta dov'è
72
+ /jev key-file # dove viene cercata la chiave
73
+ ```
74
+
75
+ Le chiavi stanno in file separati (`jev-api-key.<backend>.txt` accanto alla
76
+ config), quindi i due backend possono coesistere. Precedenza: variabile
77
+ d'ambiente → file del backend → file generico `jev.apiKeyFile` → percorso
78
+ canonico. Vedi `config/jev-config.example.json`.
79
+
57
80
  **Più twin insieme**: ogni `/jev upstream` *aggiunge* un modello guarded, senza
58
81
  rimuovere i precedenti. I twin vengono registrati anche all'avvio (i modelli
59
82
  dinamici come Codex sono letti dal models-store), quando gli originali sono disponibili.
@@ -93,10 +116,16 @@ il guarded nelle sessioni senza uno stato Jev salvato.
93
116
  Un hold `[JEV — OUTPUT NON PUBBLICATO]` interrompe intenzionalmente il turno:
94
117
  controlla `/jev last`. Non viene aggirato con un proseguimento automatico.
95
118
 
96
- Verdetto **incerto** (`review`): il gate tenta prima una riga riparazione privata
119
+ Verdetto **incerto** (`review`): il gate tenta prima una riparazione privata
97
120
  (`automatic.repairOnReview`, default `true`), poi applica la policy configurata:
98
121
  `policy.onUncertain` e `policy.onUnavailable` (`"hold"` default, oppure `"pass"`).
99
122
  Con `"pass"` il testo viene pubblicato con una nota di trasparenza sul verdetto.
123
+
124
+ Se un'altra estensione ri-registra lo stesso provider (caso reale: `pi-accounts`
125
+ al cambio account), l'overlay viene ricostruito sopra il provider live prima di
126
+ ogni turno; in più il gate toglie il suffisso `__jev` da qualunque payload,
127
+ registrando `jev-gate-bypass` e avvisando in TUI, perché quel turno non è
128
+ passato dalla verifica.
100
129
  Le conferme per comandi di rete sospetti attendono invece una decisione esplicita;
101
130
  la normale sequenza tool → risposta successiva continua senza un nuovo prompt.
102
131
 
@@ -96,8 +96,13 @@ export default function (pi: ExtensionAPI) {
96
96
  // Twin attivi, uno o più per provider. Nessun overlay finché la config non
97
97
  // li attiva (automatic) o l'utente non usa /jev upstream (opt-in).
98
98
  interface OverlayState {
99
+ /** Provider live catturato prima dell'override (auth inclusa). */
99
100
  upstream: Provider;
100
101
  twinIds: string[];
102
+ /** Il nostro overlay registrato, per accorgersi se viene scalzato. */
103
+ provider: Provider;
104
+ /** Ripiego dopo un tentativo di ri-registrazione non riuscito. */
105
+ reassertFailed?: boolean;
101
106
  }
102
107
  const overlays = new Map<string, OverlayState>();
103
108
 
@@ -208,7 +213,46 @@ export default function (pi: ExtensionAPI) {
208
213
  onVerdict: recordGateVerdict,
209
214
  });
210
215
  pi.registerProvider(overlay);
211
- overlays.set(providerId, { upstream, twinIds: [...twinIds] });
216
+ overlays.set(providerId, {
217
+ upstream,
218
+ twinIds: [...twinIds],
219
+ provider: overlay,
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Un'altra estensione (es. pi-accounts) può ri-registrare lo stesso provider
225
+ * id, sostituendo il nostro overlay con un provider che conserva i modelli
226
+ * (twin compresi) ma non il gate: l'id `__jev` finirebbe all'API.
227
+ * Qui riapplichiamo l'overlay sopra il provider live, preservandone auth e
228
+ * config. Se la ri-registrazione non regge, si smette di ritentare.
229
+ */
230
+ function reassertOverlays(registry: ModelRegistry): string[] {
231
+ const fixed: string[] = [];
232
+ for (const [providerId, state] of [...overlays]) {
233
+ if (state.reassertFailed) continue;
234
+ let live: Provider | undefined;
235
+ try {
236
+ live = registry.getProvider(providerId);
237
+ } catch {
238
+ continue;
239
+ }
240
+ if (!live || live === state.provider) continue;
241
+ try {
242
+ registerOverlay(providerId, live, state.twinIds);
243
+ const applied = overlays.get(providerId);
244
+ if (applied && registry.getProvider(providerId) === applied.provider) {
245
+ fixed.push(providerId);
246
+ } else if (applied) {
247
+ // Il registry restituisce un altro oggetto: non insistere.
248
+ applied.reassertFailed = true;
249
+ }
250
+ } catch {
251
+ const current = overlays.get(providerId);
252
+ if (current) current.reassertFailed = true;
253
+ }
254
+ }
255
+ return fixed;
212
256
  }
213
257
 
214
258
  /**
@@ -513,8 +557,62 @@ export default function (pi: ExtensionAPI) {
513
557
  });
514
558
  pi.on("session_tree", async (_event, ctx) => { await startSession(ctx); });
515
559
 
516
- // Normalize persisted aliases even after /jev off removed their overlays.
517
- // The event is a context copy: session history remains unchanged.
560
+ // Prima di ogni turno: se un'altra estensione ci ha scalzati dal provider,
561
+ // rimettiamo l'overlay (altrimenti l'id __jev arriverebbe all'API).
562
+ function noteReassert(fixed: string[]): void {
563
+ if (fixed.length === 0) return;
564
+ try {
565
+ pi.appendEntry("jev-overlay-reassert", { providers: fixed });
566
+ } catch {
567
+ // solo diagnostica
568
+ }
569
+ }
570
+
571
+ pi.on("before_agent_start", async (_event, ctx) => {
572
+ try {
573
+ noteReassert(reassertOverlays(ctx.modelRegistry));
574
+ } catch {
575
+ // mai bloccare il turno
576
+ }
577
+ });
578
+
579
+ // Ultima rete: se per qualunque motivo il payload esce con l'id sintetico,
580
+ // togliamo il suffisso (il modello originale è quello giusto) e registriamo
581
+ // che quel turno NON è passato dal gate.
582
+ let lastBypassNoticeAt = 0;
583
+ pi.on("before_provider_request", async (event, ctx) => {
584
+ try {
585
+ const payload = event.payload as { model?: unknown } | undefined;
586
+ if (!payload || typeof payload.model !== "string" || !isTwinId(payload.model)) {
587
+ return undefined;
588
+ }
589
+ const requested = payload.model;
590
+ payload.model = untwinId(requested);
591
+ try {
592
+ pi.appendEntry("jev-gate-bypass", {
593
+ requested,
594
+ sent: payload.model,
595
+ provider: ctx.model?.provider,
596
+ });
597
+ } catch {
598
+ // solo diagnostica
599
+ }
600
+ const now = Date.now();
601
+ if (ctx.hasUI && now - lastBypassNoticeAt >= 60_000) {
602
+ lastBypassNoticeAt = now;
603
+ ctx.ui.notify(
604
+ `Jev: richiesta non passata dal gate (${requested} → ${payload.model}). Verifica disattivata per questo turno; riprova per riattivarla.`,
605
+ "warning",
606
+ );
607
+ }
608
+ return payload;
609
+ } catch {
610
+ return undefined;
611
+ }
612
+ });
613
+
614
+ // Normalizza gli alias persistiti anche dopo /jev off (overlay rimossi).
615
+ // L'evento context è una copia: la cronologia salvata resta invariata.
518
616
  pi.on("context", async (event) => ({
519
617
  messages: event.messages.map((message) =>
520
618
  message.role === "assistant" && isTwinId(message.model)
@@ -526,6 +624,8 @@ export default function (pi: ExtensionAPI) {
526
624
  // su un modello non guarded (revert immediato al twin).
527
625
  pi.on("model_select", async (event, ctx) => {
528
626
  try {
627
+ // Una selezione account/provider può aver rimpiazzato il nostro overlay.
628
+ noteReassert(reassertOverlays(ctx.modelRegistry));
529
629
  if (sessionMode !== "automatic" || !config.automatic.requireGuardedModel) return;
530
630
  if (overlays.size === 0) return;
531
631
  if (isGuardedModel(event.model.provider, event.model.id)) return;
@@ -768,6 +868,9 @@ export default function (pi: ExtensionAPI) {
768
868
  pi.on("message_end", async (event, ctx) => {
769
869
  try {
770
870
  if (ctx.hasUI) ctx.ui.setStatus("jev", undefined);
871
+ // Dopo la richiesta: se qualcuno ci ha scalzati, rimetti l'overlay
872
+ // per il turno successivo (il payload resta già normalizzato).
873
+ noteReassert(reassertOverlays(ctx.modelRegistry));
771
874
  if (sessionMode !== "automatic") return;
772
875
  if (event.message.role !== "assistant") return;
773
876
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Jev validation guard for pi: on-demand tool + automatic provider gate. Dual backend: TypeSafe direct + OpenRouter.",
6
6
  "keywords": [
@@ -23,3 +23,7 @@ Run the appropriate compiler, static checks, or targeted tests separately.
23
23
  For ad-hoc typed judgements (classification, relevance, rubric scores) use
24
24
  `jev_ask` with model-defined questions instead; read p >= 0.90 as yes,
25
25
  p <= 0.20 as no, and choice/score below 0.60 confidence as uncertain.
26
+
27
+ Questa skill riguarda i tool di verifica di pi-jev-guard. Per progettare
28
+ integrazioni con TypeSafe (System One, SDK, cookbook) usa la skill ufficiale
29
+ `typesafe-ai` se installata.
package/src/commands.ts CHANGED
@@ -6,7 +6,9 @@ import { isTwinId, untwinId } from "./automatic/overlay.ts";
6
6
  import type { JevConfig, TwinRef } from "./config.ts";
7
7
  import {
8
8
  defaultConfigPath,
9
+ defaultKeyFileFor,
9
10
  expandHome,
11
+ keyFileCandidates,
10
12
  parseUpstreamRef,
11
13
  resolveBackend,
12
14
  saveConfig,
@@ -86,6 +88,7 @@ export const JEV_SUBCOMMANDS = [
86
88
  "reload",
87
89
  "output",
88
90
  "last",
91
+ "backend",
89
92
  "save-key",
90
93
  "help",
91
94
  ] as const;
@@ -108,7 +111,12 @@ export const JEV_HELP_TEXT = [
108
111
  " /jev stats Conteggi pass/block/review/unavailable + latenze",
109
112
  " /jev output Ultimo output giudicato: leak + classe errore",
110
113
  " /jev last Ultimo verdetto del gate (twin): esito e controlli",
111
- " /jev save-key Salva la chiave Jev su file (permessi 600)",
114
+ " /jev backend Mostra backend attivo e chiavi rilevate",
115
+ " /jev backend auto|openrouter|typesafe",
116
+ " Cambia backend (salvato in jev-config.json)",
117
+ " /jev save-key [backend] Salva la chiave del backend (default: attivo), 600",
118
+ " auto e typesafe tengono file separati",
119
+ " /jev key-file [backend] Mostra dove viene cercata la chiave",
112
120
  " /jev reload Ricarica jev-config.json da disco",
113
121
  " /jev help Questo aiuto",
114
122
  "",
@@ -478,11 +486,102 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
478
486
  return;
479
487
  }
480
488
 
489
+ if (sub === "backend") {
490
+ const cfg = deps.config();
491
+ const resolved = resolveBackend(cfg);
492
+ const requested = parts[1];
493
+ if (requested === undefined) {
494
+ const backendStatus = (["openrouter", "typesafe"] as const)
495
+ .map((b) => {
496
+ const key = resolveBackend({ ...cfg, jev: { ...cfg.jev, backend: b } });
497
+ const files = keyFileCandidates(cfg, b).map((f) => expandHome(f));
498
+ return ` ${b}: ${key.apiKeyPresent ? "chiave OK" : "chiave assente"} (${key.keySource})` +
499
+ (files.length > 0 ? `\n file: ${files.join(", ")}` : "");
500
+ })
501
+ .join("\n");
502
+ ctx.ui.notify(
503
+ [
504
+ `Backend: ${cfg.jev.backend} → effettivo ${resolved.backend} (${resolved.reason})`,
505
+ `Modello: ${resolved.model}`,
506
+ backendStatus,
507
+ "",
508
+ `Cambia con: /jev backend auto|openrouter|typesafe. Salva la chiave con /jev save-key ${resolved.backend}.`,
509
+ ].join("\n"),
510
+ "info",
511
+ );
512
+ return;
513
+ }
514
+ if (requested !== "auto" && requested !== "openrouter" && requested !== "typesafe") {
515
+ ctx.ui.notify("Uso: /jev backend auto|openrouter|typesafe", "warning");
516
+ return;
517
+ }
518
+ const shared = deps.sharedConfig?.() ?? cfg;
519
+ const next: JevConfig = {
520
+ ...shared,
521
+ jev: { ...shared.jev, backend: requested },
522
+ };
523
+ let savedPath: string | undefined;
524
+ try {
525
+ savedPath = saveConfig(next);
526
+ } catch (error) {
527
+ ctx.ui.notify(
528
+ `Jev: backend non salvato: ${error instanceof Error ? error.message : String(error)}`,
529
+ "error",
530
+ );
531
+ return;
532
+ }
533
+ const reloaded = deps.reload();
534
+ const after = resolveBackend(reloaded);
535
+ ctx.ui.notify(
536
+ [
537
+ `Backend impostato: ${requested} → effettivo ${after.backend} (${after.reason}).`,
538
+ `Modello: ${after.model}`,
539
+ after.apiKeyPresent
540
+ ? `Chiave: ${after.keySource}`
541
+ : `ATTENZIONE: nessuna chiave per ${requested}. Salvala con /jev save-key${requested === "auto" ? "" : ` ${requested}`}.`,
542
+ savedPath ? `Config: ${savedPath}` : "",
543
+ ]
544
+ .filter((l) => l !== "")
545
+ .join("\n"),
546
+ after.apiKeyPresent ? "info" : "warning",
547
+ );
548
+ return;
549
+ }
550
+
551
+ if (sub === "key-file") {
552
+ const cfg = deps.config();
553
+ const which = parts[1];
554
+ const backends = (["openrouter", "typesafe"] as const).filter(
555
+ (b) => which === undefined || which === b,
556
+ );
557
+ if (backends.length === 0) {
558
+ ctx.ui.notify("Uso: /jev key-file [openrouter|typesafe]", "warning");
559
+ return;
560
+ }
561
+ const lines = backends.map((b) => {
562
+ const files = keyFileCandidates(cfg, b);
563
+ return `${b}: ` + (files.length > 0
564
+ ? files.map((f) => expandHome(f)).join(" → ")
565
+ : `(nessun file; default /jev save-key ${b} → ${defaultKeyFileFor(b)})`);
566
+ });
567
+ ctx.ui.notify(
568
+ [...lines, "", "Precedenza: variabile d'ambiente, poi file specifico del backend, poi il file generico."].join("\n"),
569
+ "info",
570
+ );
571
+ return;
572
+ }
573
+
481
574
  if (sub === "save-key") {
482
575
  const cfg = deps.config();
483
- let target = cfg.jev.apiKeyFile?.trim()
484
- ? expandHome(cfg.jev.apiKeyFile.trim())
485
- : resolve(dirname(defaultConfigPath()), "jev-api-key.txt");
576
+ const which = parts[1];
577
+ const backend: "openrouter" | "typesafe" =
578
+ which === "openrouter" || which === "typesafe"
579
+ ? which
580
+ : resolveBackend(cfg).backend;
581
+ // Le chiavi sono per-backend: auto e typesafe possono coesistere.
582
+ let target = cfg.jev.apiKeyFiles?.[backend]?.trim()
583
+ ? expandHome(cfg.jev.apiKeyFiles[backend]!.trim())
584
+ : defaultKeyFileFor(backend);
486
585
  // Mai chiavi in tmp: dir effimera (pulizia automatica = chiave persa).
487
586
  // Può capitare con config stantie caricate in memoria prima di un fix.
488
587
  if (target === tmpdir() || target.startsWith(tmpdir() + "/")) {
@@ -490,12 +589,12 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
490
589
  `Jev: percorso chiave sotto tmp ignorato (${target}); uso quello canonico.`,
491
590
  "warning",
492
591
  );
493
- target = resolve(dirname(defaultConfigPath()), "jev-api-key.txt");
592
+ target = defaultKeyFileFor(backend);
494
593
  }
495
594
  let key: string | undefined;
496
595
  try {
497
596
  key = await ctx.ui.input(
498
- "Jev API key",
597
+ `Jev API key (${backend})`,
499
598
  "Incolla la chiave (potrebbe fare echo nel terminale)",
500
599
  );
501
600
  } catch {
@@ -514,20 +613,31 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
514
613
  ctx.ui.notify(`Jev: salvataggio fallito: ${error instanceof Error ? error.message : String(error)}`, "error");
515
614
  return;
516
615
  }
517
- // Punta la config al file e ricarica gli handle col nuovo stato.
518
- cfg.jev.apiKeyFile = target;
616
+ // Punta la config al file del backend e ricarica gli handle.
617
+ const shared = deps.sharedConfig?.() ?? cfg;
618
+ const next: JevConfig = {
619
+ ...shared,
620
+ jev: {
621
+ ...shared.jev,
622
+ apiKeyFiles: { ...shared.jev.apiKeyFiles, [backend]: target },
623
+ },
624
+ };
519
625
  try {
520
- saveConfig({ ...cfg, mode: deps.sharedConfig?.().mode ?? cfg.mode });
626
+ saveConfig(next);
521
627
  } catch {
522
628
  // Resta in memoria anche se il salvataggio fallisce.
523
629
  }
524
630
  const reloaded = deps.reload();
525
631
  const resolved = resolveBackend(reloaded);
632
+ const legacyNote =
633
+ shared.jev.apiKeyFile && shared.jev.apiKeyFile !== target
634
+ ? ` Nota: resta anche il file generico ${expandHome(shared.jev.apiKeyFile)} (usato come ripiego).`
635
+ : "";
526
636
  ctx.ui.notify(
527
- resolved.apiKeyPresent
528
- ? `Jev: chiave salvata in ${target} (600). Sorgente: ${resolved.keySource}.`
529
- : `Jev: file scritto ma chiave non rilevata (${resolved.keySource}).`,
530
- resolved.apiKeyPresent ? "info" : "warning",
637
+ resolved.backend === backend && resolved.apiKeyPresent
638
+ ? `Jev: chiave ${backend} salvata in ${target} (600). Sorgente: ${resolved.keySource}.${legacyNote}`
639
+ : `Jev: file scritto, ma il backend effettivo è ${resolved.backend} (${resolved.keySource}).${legacyNote}`,
640
+ resolved.backend === backend && resolved.apiKeyPresent ? "info" : "warning",
531
641
  );
532
642
  return;
533
643
  }
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import type { JevBackend } from "./reviewer.ts";
5
5
 
6
6
  export type JevMode = "on-demand" | "automatic";
@@ -20,6 +20,8 @@ export interface JevConfig {
20
20
  retryTransients: { enabled: boolean; maxRetries: number };
21
21
  /** Percorso file con la chiave per il backend attivo (alternativa all'env, `~/` espanso). */
22
22
  apiKeyFile?: string;
23
+ /** File chiave dedicati per backend: permettono di tenerle entrambe. */
24
+ apiKeyFiles?: { openrouter?: string; typesafe?: string };
23
25
  };
24
26
  policy: {
25
27
  revision: string;
@@ -277,28 +279,69 @@ interface BackendKey {
277
279
  source: string;
278
280
  }
279
281
 
282
+ /**
283
+ * File chiave da provare, in ordine: specifico del backend, generico,
284
+ * poi il percorso canonico per-backend (chiave lasciata lì senza config).
285
+ */
286
+ /** Solo i file dichiarati in config (errori di lettura da segnalare). */
287
+ export function configuredKeyFiles(config: JevConfig, backend: JevBackend): string[] {
288
+ const paths: string[] = [];
289
+ const push = (path: string | undefined) => {
290
+ const trimmed = path?.trim();
291
+ if (trimmed && !paths.includes(trimmed)) paths.push(trimmed);
292
+ };
293
+ push(config.jev.apiKeyFiles?.[backend]);
294
+ push(config.jev.apiKeyFile);
295
+ return paths;
296
+ }
297
+
298
+ export function keyFileCandidates(config: JevConfig, backend: JevBackend): string[] {
299
+ const paths = configuredKeyFiles(config, backend);
300
+ const canonical = defaultKeyFileFor(backend);
301
+ if (!paths.includes(canonical)) paths.push(canonical);
302
+ return paths;
303
+ }
304
+
280
305
  function resolveKeyFor(
281
306
  backend: JevBackend,
282
- apiKeyFile: string | undefined,
307
+ files: { candidates: string[]; configured: string[] },
283
308
  ): BackendKey {
284
309
  const envVar = backend === "typesafe" ? "TYPESAFE_API_KEY" : "OPENROUTER_API_KEY";
285
310
  const fromEnv = process.env[envVar]?.trim();
286
311
  if (fromEnv) return { key: fromEnv, source: `env:${envVar}` };
287
- const file = readKeyFile(apiKeyFile);
288
- if (file.key) return { key: file.key, source: `file:${file.tried}` };
289
- if (file.tried) {
290
- return { key: undefined, source: `none (file ${file.tried}: ${file.error})` };
312
+
313
+ const problems: string[] = [];
314
+ for (const path of files.candidates) {
315
+ const file = readKeyFile(path);
316
+ if (file.key) return { key: file.key, source: `file:${file.tried}` };
317
+ // Il percorso canonico mancante è atteso: non è un problema da mostrare.
318
+ if (file.error && files.configured.includes(path)) {
319
+ problems.push(`file ${file.tried}: ${file.error}`);
320
+ }
291
321
  }
292
- return { key: undefined, source: `none (set ${envVar} or jev.apiKeyFile)` };
322
+ if (problems.length > 0) {
323
+ return { key: undefined, source: `none (${problems.join("; ")})` };
324
+ }
325
+ return {
326
+ key: undefined,
327
+ source: `none (set ${envVar} or /jev save-key ${backend})`,
328
+ };
329
+ }
330
+
331
+ /** Percorso canonico del file chiave per un backend (accanto alla config). */
332
+ export function defaultKeyFileFor(backend: JevBackend): string {
333
+ return resolve(dirname(defaultConfigPath()), `jev-api-key.${backend}.txt`);
293
334
  }
294
335
 
295
336
  export function resolveBackend(config: JevConfig): ResolvedBackend {
296
337
  const setting = config.jev.backend;
297
- const keyFile = config.jev.apiKeyFile;
298
338
 
299
339
  if (setting === "typesafe" || setting === "openrouter") {
300
340
  const backend = setting;
301
- const resolved = resolveKeyFor(backend, keyFile);
341
+ const resolved = resolveKeyFor(backend, {
342
+ candidates: keyFileCandidates(config, backend),
343
+ configured: configuredKeyFiles(config, backend),
344
+ });
302
345
  return {
303
346
  backend,
304
347
  model:
@@ -317,8 +360,14 @@ export function resolveBackend(config: JevConfig): ResolvedBackend {
317
360
  }
318
361
 
319
362
  // auto: preferisci backend con chiave disponibile, default openrouter.
320
- const openRouterKey = resolveKeyFor("openrouter", keyFile);
321
- const typesafeKey = resolveKeyFor("typesafe", keyFile);
363
+ const openRouterKey = resolveKeyFor("openrouter", {
364
+ candidates: keyFileCandidates(config, "openrouter"),
365
+ configured: configuredKeyFiles(config, "openrouter"),
366
+ });
367
+ const typesafeKey = resolveKeyFor("typesafe", {
368
+ candidates: keyFileCandidates(config, "typesafe"),
369
+ configured: configuredKeyFiles(config, "typesafe"),
370
+ });
322
371
  const hasOpenRouter = Boolean(openRouterKey.key);
323
372
  const hasTypesafe = Boolean(typesafeKey.key);
324
373