]> git.99rst.org Git - sgasser-llm-shield.git/commitdiff
Add configurable detector timeout (#137)
authorStefan Gasser <redacted>
Fri, 3 Jul 2026 09:38:01 +0000 (11:38 +0200)
committerGitHub <redacted>
Fri, 3 Jul 2026 09:38:01 +0000 (11:38 +0200)
12 files changed:
config.example.yaml
docs/api-reference/status.mdx
docs/configuration/overview.mdx
docs/configuration/pii-detection.mdx
docs/installation.mdx
src/config.test.ts
src/config.ts
src/logging/log-content.test.ts
src/pii/detect.test.ts
src/pii/detect.ts
src/routes/info.test.ts
src/routes/info.ts

index 49e113d5b2f6e5b0229d2d0bede79e2bcb85476f..5425b6f57b7903a7e9318e1fc2146c41e4c228e0 100644 (file)
@@ -71,6 +71,10 @@ pii_detection:
   # for local dev (bun run dev against the docker-compose detector service).
   detector_url: ${DETECTOR_URL:-http://localhost:5002}
 
+  # Timeout for each detector /analyze request in seconds.
+  # Increase this for very large messages; set to 0 to disable.
+  detector_timeout: ${DETECTOR_TIMEOUT:-30}
+
   # Add regions only if you need national-format numbers; + numbers work globally.
   phone_regions: []
   # phone_regions: [US, GB, DE, IT, IN]
index 6d3d16e243278dae330485e2c4adee993e0b9d2d..9e56fdf7b445f06e939bbe6bac966cf0d2997611 100644 (file)
@@ -82,6 +82,7 @@ curl http://localhost:3000/info
   },
   "pii_detection": {
     "phone_regions": [],
+    "detector_timeout": 30,
     "score_threshold": 0.7,
     "entities": ["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER"]
   },
index 674a55cca3a4726114ef8f83d234652d5ca70b0f..9fd1af1f5026c88870f418dcd44d41c6851c8d06 100644 (file)
@@ -66,4 +66,5 @@ providers:
 
 pii_detection:
   detector_url: ${DETECTOR_URL:-http://localhost:5002}
+  detector_timeout: ${DETECTOR_TIMEOUT:-30}
 ```
index 0717bb757f0ad67a67644b205336f9c1f77d6315..0c2f99f19ba9c4b80371bd4b7788e73ded7b366f 100644 (file)
@@ -6,6 +6,7 @@ description: Configure PII detection settings
 ```yaml
 pii_detection:
   detector_url: http://localhost:5002
+  detector_timeout: 30
   phone_regions: []
   score_threshold: 0.7
   entities:
@@ -24,6 +25,7 @@ pii_detection:
 | Option | Default | Description |
 |--------|---------|-------------|
 | `detector_url` | `http://localhost:5002` | Detector `/analyze` URL |
+| `detector_timeout` | `30` | Timeout in seconds for each detector `/analyze` request. Increase for very large messages; set to `0` to disable |
 | `phone_regions` | `[]` | Optional regions for national-format phone numbers |
 | `score_threshold` | `0.7` | Minimum confidence floor for the neural labels PERSON and LOCATION (0.0-1.0). Checksum-validated identifiers always score `1.0` and are unaffected |
 | `entities` | See below | Entity types to return |
index 20783baefff2327633c23a9ece47ef1e9002e04d..e14a365763c49726ef421d76754d343aaee4fbb3 100644 (file)
@@ -78,6 +78,7 @@ country prefix — see [PII Detection Config](/configuration/pii-detection).
 | Variable | Default | Description |
 |----------|---------|-------------|
 | `DETECTOR_URL` | `http://localhost:5002` | Where the proxy reaches the detector. In the all-in-one image this is in-container and rarely changed; override it to point at an external detector. |
+| `DETECTOR_TIMEOUT` | `30` | Seconds to wait for each detector `/analyze` request when using the example config. Increase for very large messages; set to `0` to disable. |
 | `PASTEGUARD_STARTUP_TIMEOUT` | `180` | Seconds to wait for the detector to become ready at startup |
 
 ## Next Steps
index 58bc12437c5354dde9040afe99f12e8fdb066d31..54c329084b951ac44a2f33b7a1ab1d0e9bf23bbf 100644 (file)
@@ -225,6 +225,45 @@ pii_detection:
     }
   });
 
+  test("defaults PII detector timeout to 30 seconds", () => {
+    const path = writeConfig(`
+mode: mask
+providers:
+  openai: {}
+  anthropic: {}
+pii_detection:
+  detector_url: http://localhost:5002
+`);
+
+    try {
+      const config = loadConfig(path);
+
+      expect(config.pii_detection.detector_timeout).toBe(30);
+    } finally {
+      cleanupConfig(path);
+    }
+  });
+
+  test("accepts PII detector timeout override", () => {
+    const path = writeConfig(`
+mode: mask
+providers:
+  openai: {}
+  anthropic: {}
+pii_detection:
+  detector_url: http://localhost:5002
+  detector_timeout: \${DETECTOR_TIMEOUT:-300}
+`);
+
+    try {
+      const config = loadConfig(path);
+
+      expect(config.pii_detection.detector_timeout).toBe(300);
+    } finally {
+      cleanupConfig(path);
+    }
+  });
+
   test("defaults request logging to SQLite", () => {
     const path = writeConfig(`
 mode: mask
index 1f7e4cc69048b54afafb9908b31012ffe5be3e47..0357e23fad8236ea870388aa03ecec0fc414452f 100644 (file)
@@ -130,6 +130,7 @@ const scanRolesField = z
 const PIIDetectionSchema = z.object({
   enabled: z.boolean().default(true),
   detector_url: z.string().url(),
+  detector_timeout: z.coerce.number().int().min(0).default(30),
   phone_regions: PhoneRegionsSchema,
   score_threshold: z.coerce.number().min(0).max(1).default(0.7),
   entities: z
index 987655664b89b09c026b61cf3f27ba3971b9d105..bda2ded8e560230864561fecb9030c188756db35 100644 (file)
@@ -219,6 +219,7 @@ describe("formatMaskedRequestForLog", () => {
       pii_detection: {
         enabled: true,
         detector_url: "http://localhost:8080",
+        detector_timeout: 30,
         phone_regions: [],
         score_threshold: 0.7,
         entities: ["EMAIL_ADDRESS"],
index 3ef632606be46bd1c5d263bc3b28285cc742ae63..563afc05de2036068ddcadaa21b538a0a8dfe498 100644 (file)
@@ -407,6 +407,57 @@ describe("PIIDetector", () => {
   });
 
   describe("detectPII", () => {
+    test("uses the configured detector timeout", async () => {
+      const config = getConfig();
+      const previousTimeout = config.pii_detection.detector_timeout;
+      const originalTimeout = AbortSignal.timeout;
+      let timeoutMs: number | undefined;
+      config.pii_detection.detector_timeout = 300;
+      AbortSignal.timeout = mock((ms: number) => {
+        timeoutMs = ms;
+        return undefined as unknown as AbortSignal;
+      }) as unknown as typeof AbortSignal.timeout;
+      mockDetector({});
+
+      try {
+        const detector = new PIIDetector();
+        await detector.detectPII("Hello world");
+
+        expect(timeoutMs).toBe(300_000);
+      } finally {
+        config.pii_detection.detector_timeout = previousTimeout;
+        AbortSignal.timeout = originalTimeout;
+      }
+    });
+
+    test("disables detector request timeout when configured as 0", async () => {
+      const config = getConfig();
+      const previousTimeout = config.pii_detection.detector_timeout;
+      let signal: AbortSignal | null | undefined;
+      config.pii_detection.detector_timeout = 0;
+
+      globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
+        if (url.toString().includes("/analyze")) {
+          signal = init?.signal;
+          return new Response(JSON.stringify([]), {
+            status: 200,
+            headers: { "Content-Type": "application/json" },
+          });
+        }
+
+        return originalFetch(url, init);
+      }) as unknown as typeof fetch;
+
+      try {
+        const detector = new PIIDetector();
+        await detector.detectPII("Hello world");
+
+        expect(signal).toBeUndefined();
+      } finally {
+        config.pii_detection.detector_timeout = previousTimeout;
+      }
+    });
+
     test("returns entities from the detector", async () => {
       mockDetector({
         "test@example.com": [{ entity_type: "EMAIL_ADDRESS", start: 0, end: 16, score: 0.99 }],
index 957e680cbbbac51d2268170b4f0296fa43bb9b20..f4ccf70249f8abf1d5175ac49c12aa01f27314a7 100644 (file)
@@ -144,6 +144,7 @@ export interface PIIDetectionResult {
 
 export class PIIDetector {
   private detectorUrl: string;
+  private detectorTimeoutMs: number;
   private scoreThreshold: number;
   private entityTypes: string[];
   private phoneRegions: string[];
@@ -151,6 +152,7 @@ export class PIIDetector {
   constructor() {
     const config = getConfig();
     this.detectorUrl = config.pii_detection.detector_url;
+    this.detectorTimeoutMs = config.pii_detection.detector_timeout * 1000;
     this.scoreThreshold = config.pii_detection.score_threshold;
     this.entityTypes = config.pii_detection.entities;
     this.phoneRegions = config.pii_detection.phone_regions;
@@ -173,7 +175,8 @@ export class PIIDetector {
           "Content-Type": "application/json",
         },
         body: JSON.stringify(request),
-        signal: AbortSignal.timeout(30_000),
+        signal:
+          this.detectorTimeoutMs > 0 ? AbortSignal.timeout(this.detectorTimeoutMs) : undefined,
       });
 
       if (!response.ok) {
index a07d935270180ced3ac1168b5006a06849d62237..3ec8c92cfdbdef50b0951be84b5785b8c5fe4523 100644 (file)
@@ -21,6 +21,7 @@ describe("GET /info", () => {
       ((body.providers as Record<string, { base_url: string }>).codex.base_url as string).length,
     ).toBeGreaterThan(0);
     expect(body.pii_detection).toBeDefined();
+    expect((body.pii_detection as Record<string, unknown>).detector_timeout).toBeDefined();
   });
 
   test("includes secrets_detection and logging sections", async () => {
index fe1a3444128ff4d142979ff8842698a9cdab09ef..e8c093c98177f545e335b28f823d605f8760fca8 100644 (file)
@@ -30,6 +30,7 @@ infoRoutes.get("/info", (c) => {
     providers,
     pii_detection: {
       phone_regions: config.pii_detection.phone_regions,
+      detector_timeout: config.pii_detection.detector_timeout,
       score_threshold: config.pii_detection.score_threshold,
       entities: config.pii_detection.entities,
     },
git clone https://git.99rst.org/PROJECT