{"version":3,"file":"moderation.mjs","names":[],"sources":["../../../../../../../../ai/src/guard/detectors/moderation.ts"],"sourcesContent":["import type {\n  GuardrailDetector,\n  GuardrailMatch,\n  GuardrailVerdict,\n  OpenAiClientLike,\n  OpenAiModerationOptions,\n  OpenAiModerationResult,\n} from \"../contracts\";\nimport { OPENAI_INSTALL_INSTRUCTIONS } from \"../errors\";\n\nconst DETECTOR_NAME = \"moderation.openai\";\n\nconst DEFAULT_MODEL = \"omni-moderation-latest\";\n\n// ============================================================\n// Lazily-loaded openai SDK (OPTIONAL peer)\n// ============================================================\n\nlet OpenAiSdk: typeof import(\"openai\");\nlet isModuleExists: boolean | undefined;\nlet loadingPromise: Promise<void> | undefined;\n\n/**\n * Settle the lazy import of `openai` once, concurrency-safe. Only needed\n * when the caller did not pass a ready `client`. A bare `catch` flips the\n * flag to `false`; the curated {@link OPENAI_INSTALL_INSTRUCTIONS} surfaces\n * at first `check()`, never a raw module-resolution stack trace. Mirrors\n * ai-panoptic's `loadLangfuse`.\n */\nfunction loadOpenAi(): Promise<void> {\n  if (isModuleExists !== undefined) {\n    return Promise.resolve();\n  }\n\n  if (loadingPromise) {\n    return loadingPromise;\n  }\n\n  loadingPromise = (async () => {\n    try {\n      OpenAiSdk = await import(\"openai\");\n      isModuleExists = true;\n    } catch {\n      isModuleExists = false;\n    }\n  })();\n\n  return loadingPromise;\n}\n\n/**\n * The optional OpenAI-backed moderation detector — the internal class behind\n * the {@link moderation} factory. Sends the inspected text to OpenAI's\n * moderation endpoint and maps the flagged categories to a verdict: any\n * category in `blockOn` → `block`; any other flagged category → `flag`;\n * nothing flagged → `allow`.\n *\n * The `openai` SDK is resolved lazily on the FIRST `check()` (not at\n * construction) so importing `@warlock.js/ai` never forces the peer to\n * be installed. When a `client` is supplied it is used verbatim and the SDK\n * is never imported.\n */\nclass OpenAiModerationDetector implements GuardrailDetector {\n  public readonly name = DETECTOR_NAME;\n\n  /** A pre-built client, or `undefined` until the lazy SDK constructs one. */\n  private client: OpenAiClientLike | undefined;\n\n  private readonly apiKey: string | undefined;\n\n  private readonly model: string;\n\n  /** Categories that escalate to `block`; empty means \"flag on any\". */\n  private readonly blockOn: ReadonlySet<string>;\n\n  public constructor(options: OpenAiModerationOptions = {}) {\n    this.client = options.client;\n    this.apiKey = options.apiKey;\n    this.model = options.model ?? DEFAULT_MODEL;\n    this.blockOn = new Set(options.blockOn ?? []);\n\n    // Kick off the lazy import eagerly when no client was supplied, so the\n    // first `check()` does not pay the resolution latency. Errors are\n    // swallowed by `loadOpenAi`; the curated install string surfaces at use.\n    if (!this.client) {\n      loadOpenAi();\n    }\n  }\n\n  /**\n   * Moderate `text` and fold the response into a verdict. `allow` when the\n   * model flags nothing; `block` when any flagged category is in `blockOn`;\n   * otherwise `flag` listing every flagged category. Resolving the client\n   * throws the curated install string when the `openai` peer is absent.\n   */\n  public async check(text: string): Promise<GuardrailVerdict> {\n    const client = await this.resolveClient();\n\n    const response = await client.moderations.create({\n      model: this.model,\n      input: text,\n    });\n\n    const result = response.results[0];\n\n    if (result === undefined || !result.flagged) {\n      return { type: \"allow\" };\n    }\n\n    return this.toVerdict(result);\n  }\n\n  /**\n   * Return the supplied client, or construct one lazily from the resolved\n   * SDK. Throws {@link OPENAI_INSTALL_INSTRUCTIONS} (a plain `Error` — a\n   * missing optional peer is an infrastructure fault, not a content\n   * violation) when `openai` could not be imported.\n   */\n  private async resolveClient(): Promise<OpenAiClientLike> {\n    if (this.client) {\n      return this.client;\n    }\n\n    await loadOpenAi();\n\n    if (!isModuleExists) {\n      throw new Error(OPENAI_INSTALL_INSTRUCTIONS);\n    }\n\n    this.client = new OpenAiSdk.default({\n      apiKey: this.apiKey,\n    }) as unknown as OpenAiClientLike;\n\n    return this.client;\n  }\n\n  /**\n   * Fold a flagged moderation result into a `block` or `flag` verdict. Every\n   * `true` category becomes a {@link GuardrailMatch} (`moderation.<category>`);\n   * the verdict is `block` when any flagged category is in `blockOn`,\n   * otherwise `flag`.\n   */\n  private toVerdict(result: OpenAiModerationResult): GuardrailVerdict {\n    const flagged = Object.entries(result.categories)\n      .filter(([, tripped]) => tripped)\n      .map(([category]) => category);\n\n    const matches: GuardrailMatch[] = flagged.map((category) => ({\n      rule: `moderation.${category}`,\n      label: category,\n    }));\n\n    const shouldBlock = flagged.some((category) => this.blockOn.has(category));\n    const list = flagged.join(\", \");\n\n    if (shouldBlock) {\n      return {\n        type: \"block\",\n        reason: `OpenAI moderation flagged blocked category(ies): ${list}.`,\n        matches,\n      };\n    }\n\n    return {\n      type: \"flag\",\n      reason: `OpenAI moderation flagged category(ies): ${list}.`,\n      matches,\n    };\n  }\n}\n\n/**\n * Build the optional `moderation` detector (surfaced as\n * `ai.guardrail.moderation(options?)`), backed by OpenAI's moderation\n * endpoint. The `openai` SDK is an **optional lazy peer**: importing\n * `@warlock.js/ai` never forces it to resolve, and the detector throws\n * a curated install string ({@link OPENAI_INSTALL_INSTRUCTIONS}) on first\n * `check()` when the peer is absent — mirroring ai-panoptic's lazy Langfuse\n * exporter.\n *\n * On a moderation hit, every flagged category becomes a\n * {@link GuardrailMatch}; the verdict is `block` when any flagged category is\n * listed in `blockOn`, otherwise `flag`. A clean result is `allow`.\n *\n * @param options - `apiKey` (defaults to `OPENAI_API_KEY`), `model`\n *   (defaults to `\"omni-moderation-latest\"`), `blockOn` (categories that\n *   escalate to `block`), or a pre-built `client` to bypass the lazy import.\n * @returns A {@link GuardrailDetector} for the guard's `input` / `output` / `tool` arrays.\n *\n * @example\n * const guard = ai.guardrail({\n *   output: [\n *     ai.guardrail.moderation({ blockOn: [\"violence\", \"sexual/minors\"] }),\n *   ],\n * });\n */\nexport function moderation(\n  options?: OpenAiModerationOptions,\n): GuardrailDetector {\n  return new OpenAiModerationDetector(options);\n}\n"],"mappings":";;;AAUA,MAAM,gBAAgB;AAEtB,MAAM,gBAAgB;AAMtB,IAAI;AACJ,IAAI;AACJ,IAAI;;;;;;;;AASJ,SAAS,aAA4B;CACnC,IAAI,mBAAmB,QACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,YAAY,MAAM,OAAO;GACzB,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;AAcA,IAAM,2BAAN,MAA4D;CAa1D,AAAO,YAAY,UAAmC,CAAC,GAAG;cAZnC;EAarB,KAAK,SAAS,QAAQ;EACtB,KAAK,SAAS,QAAQ;EACtB,KAAK,QAAQ,QAAQ,SAAS;EAC9B,KAAK,UAAU,IAAI,IAAI,QAAQ,WAAW,CAAC,CAAC;EAK5C,IAAI,CAAC,KAAK,QACR,WAAW;CAEf;;;;;;;CAQA,MAAa,MAAM,MAAyC;EAQ1D,MAAM,UAAS,OALQ,MAFF,KAAK,cAAc,EAEX,CAAC,YAAY,OAAO;GAC/C,OAAO,KAAK;GACZ,OAAO;EACT,CAAC,EAEsB,CAAC,QAAQ;EAEhC,IAAI,WAAW,UAAa,CAAC,OAAO,SAClC,OAAO,EAAE,MAAM,QAAQ;EAGzB,OAAO,KAAK,UAAU,MAAM;CAC9B;;;;;;;CAQA,MAAc,gBAA2C;EACvD,IAAI,KAAK,QACP,OAAO,KAAK;EAGd,MAAM,WAAW;EAEjB,IAAI,CAAC,gBACH,MAAM,IAAI,MAAM,2BAA2B;EAG7C,KAAK,SAAS,IAAI,UAAU,QAAQ,EAClC,QAAQ,KAAK,OACf,CAAC;EAED,OAAO,KAAK;CACd;;;;;;;CAQA,AAAQ,UAAU,QAAkD;EAClE,MAAM,UAAU,OAAO,QAAQ,OAAO,UAAU,CAAC,CAC9C,QAAQ,GAAG,aAAa,OAAO,CAAC,CAChC,KAAK,CAAC,cAAc,QAAQ;EAE/B,MAAM,UAA4B,QAAQ,KAAK,cAAc;GAC3D,MAAM,cAAc;GACpB,OAAO;EACT,EAAE;EAEF,MAAM,cAAc,QAAQ,MAAM,aAAa,KAAK,QAAQ,IAAI,QAAQ,CAAC;EACzE,MAAM,OAAO,QAAQ,KAAK,IAAI;EAE9B,IAAI,aACF,OAAO;GACL,MAAM;GACN,QAAQ,oDAAoD,KAAK;GACjE;EACF;EAGF,OAAO;GACL,MAAM;GACN,QAAQ,4CAA4C,KAAK;GACzD;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,WACd,SACmB;CACnB,OAAO,IAAI,yBAAyB,OAAO;AAC7C"}