feat(gateway): Jev (System One) as primary text moderator with LLM fallback (#80)

* feat(gateway): Jev (System One) as primary text moderator with LLM fallback

Add TypeSafe Jev via @typesafe-ai/sdk v0.6.0 as the PRIMARY analyzer for
text-only moderation sub-batches; the existing LLM stays as the fallback
for anything Jev cannot decide confidently (per-message gate rejection or
API failure) and for media batches.

- jevAnalyzer.ts: TypeSafeClient wrapper, declarative state builder
  (System One models MUST get factual state, not chat-XML — chat framing
  made Jev confidently wrong on clean messages at 0.98 confidence),
  message-id-keyed question builder (5 typed questions per message),
  cross-consistency acceptance gate (noul↔status↔severity↔action↔category),
  answer→AnalysisResult mapper, fail-open outcome.
- textBatchProcessor.ts: Jev-first per sub-batch, rejected ids + API
  failures fall back to callModerationLLM; no cross-batch pollution.
- config: AI_LLM_JEV_ENABLED/API_KEY/BASE_URL/MODEL/TIMEOUT_MS/MIN_CONFIDENCE.
- .env.example: documented all 6 Jev env vars.
- tests: unit (question/state builders, gate, mapper) + live smoke
  (gated behind AI_LLM_JEV_SMOKE=1) verified 4/4 accepted vs real 9router.

* fix: auto-fix code quality [skip ci]
This commit is contained in:
asepharyana
2026-09-23 17:51:38 +07:00
committed by GitHub
parent 5ba891ecfb
commit b9bba643bd
8 changed files with 1217 additions and 52 deletions
@@ -193,6 +193,23 @@ export const configSchema = z
// (AI_LLM_BASE_URL) but a different model alias. The dedicated NVIDIA
// multimodal endpoint was removed.
AI_LLM_VISION_MODEL: z.string().default("multimodal"),
// ── Jev (TypeSafe System One) — primary text analyzer ────────────────
// Jev evaluates typed questions against a state and returns calibrated
// structured answers (no text generation). Runs on 9router's
// /v1/systemone (free model oc/jev-1.13-free). The existing LLM remains
// the fallback for anything Jev cannot decide confidently and for media.
AI_LLM_JEV_ENABLED: z
.string()
.optional()
.transform((v) => v === "true")
.default(true),
AI_LLM_JEV_API_KEY: z.string().optional().default(""),
AI_LLM_JEV_BASE_URL: z.string().url().default("http://127.0.0.1:4014"),
AI_LLM_JEV_MODEL: z.string().default("oc/jev-1.13-free"),
// Per-message acceptance threshold on the status choice confidence.
// Below this the message falls back to the LLM (fail-open).
AI_LLM_JEV_MIN_CONFIDENCE: z.coerce.number().min(0).max(1).default(0.9),
AI_LLM_JEV_TIMEOUT_MS: z.coerce.number().int().positive().default(45_000),
AI_LLM_DISABLE_THINKING: z
.string()
.default("true")