From: Stefan Gasser Date: Fri, 3 Jul 2026 09:38:01 +0000 (+0200) Subject: Add configurable detector timeout (#137) X-Git-Tag: v0.7.5~1 X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=8a424895654edcb7074135a9f66c8221cd597bf3;p=sgasser-llm-shield.git Add configurable detector timeout (#137) --- diff --git a/config.example.yaml b/config.example.yaml index 49e113d..5425b6f 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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] diff --git a/docs/api-reference/status.mdx b/docs/api-reference/status.mdx index 6d3d16e..9e56fdf 100644 --- a/docs/api-reference/status.mdx +++ b/docs/api-reference/status.mdx @@ -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"] }, diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx index 674a55c..9fd1af1 100644 --- a/docs/configuration/overview.mdx +++ b/docs/configuration/overview.mdx @@ -66,4 +66,5 @@ providers: pii_detection: detector_url: ${DETECTOR_URL:-http://localhost:5002} + detector_timeout: ${DETECTOR_TIMEOUT:-30} ``` diff --git a/docs/configuration/pii-detection.mdx b/docs/configuration/pii-detection.mdx index 0717bb7..0c2f99f 100644 --- a/docs/configuration/pii-detection.mdx +++ b/docs/configuration/pii-detection.mdx @@ -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 | diff --git a/docs/installation.mdx b/docs/installation.mdx index 20783ba..e14a365 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -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 diff --git a/src/config.test.ts b/src/config.test.ts index 58bc124..54c3290 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -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 diff --git a/src/config.ts b/src/config.ts index 1f7e4cc..0357e23 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 diff --git a/src/logging/log-content.test.ts b/src/logging/log-content.test.ts index 9876556..bda2ded 100644 --- a/src/logging/log-content.test.ts +++ b/src/logging/log-content.test.ts @@ -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"], diff --git a/src/pii/detect.test.ts b/src/pii/detect.test.ts index 3ef6326..563afc0 100644 --- a/src/pii/detect.test.ts +++ b/src/pii/detect.test.ts @@ -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 }], diff --git a/src/pii/detect.ts b/src/pii/detect.ts index 957e680..f4ccf70 100644 --- a/src/pii/detect.ts +++ b/src/pii/detect.ts @@ -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) { diff --git a/src/routes/info.test.ts b/src/routes/info.test.ts index a07d935..3ec8c92 100644 --- a/src/routes/info.test.ts +++ b/src/routes/info.test.ts @@ -21,6 +21,7 @@ describe("GET /info", () => { ((body.providers as Record).codex.base_url as string).length, ).toBeGreaterThan(0); expect(body.pii_detection).toBeDefined(); + expect((body.pii_detection as Record).detector_timeout).toBeDefined(); }); test("includes secrets_detection and logging sections", async () => { diff --git a/src/routes/info.ts b/src/routes/info.ts index fe1a344..e8c093c 100644 --- a/src/routes/info.ts +++ b/src/routes/info.ts @@ -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, },