feat: add configurable request timeout (#79)
authorStefan Gasser <redacted>
Wed, 4 Mar 2026 08:28:29 +0000 (09:28 +0100)
committerGitHub <redacted>
Wed, 4 Mar 2026 08:28:29 +0000 (09:28 +0100)
Add server.request_timeout config option (default: 600 seconds).
Previously hardcoded to 120 seconds which caused timeouts for
complex Opus queries.

Set to 0 to disable timeout entirely.

Closes #78

config.example.yaml
docs/configuration/overview.mdx
src/config.ts
src/constants/timeouts.ts
src/providers/anthropic/client.ts
src/providers/local.ts
src/providers/openai/client.ts

index 3c20aaf3dfca1cb44078529f654b18fe04a25b76..07ebd73cc7f7ad7c04a1b36e8c2c324d31d0245b 100644 (file)
@@ -11,6 +11,7 @@ mode: mask
 server:
   port: 3000
   host: "0.0.0.0"
+  # request_timeout: 600  # Seconds (0 = no timeout, default: 600)
 
 # Providers - API endpoints
 # Can be cloud (OpenAI, Anthropic, Azure) or self-hosted (vLLM, LiteLLM proxy, etc.)
index 74674a185edf37793cdd5807215c84da96215e7a..96130df58f8709a7bc31afc7aedc27c94c0957ce 100644 (file)
@@ -30,12 +30,14 @@ See [Mask Mode](/concepts/mask-mode) and [Route Mode](/concepts/route-mode) for
 server:
   port: 3000
   host: "0.0.0.0"
+  request_timeout: 600
 ```
 
 | Option | Default | Description |
 |--------|---------|-------------|
 | `port` | `3000` | HTTP port |
 | `host` | `0.0.0.0` | Bind address |
+| `request_timeout` | `600` | Request timeout in seconds (0 = no timeout) |
 
 ## Dashboard
 
index 00e24ab89155a20718ce6cbfe16e337c49d848bb..29457c602946828223b838dde234f4877e356472 100644 (file)
@@ -72,6 +72,7 @@ const PIIDetectionSchema = z.object({
 const ServerSchema = z.object({
   port: z.coerce.number().int().min(1).max(65535).default(3000),
   host: z.string().default("0.0.0.0"),
+  request_timeout: z.coerce.number().int().min(0).default(600),
 });
 
 const LoggingSchema = z.object({
@@ -163,6 +164,7 @@ export type AnthropicProviderConfig = z.infer<typeof AnthropicProviderSchema>;
 export type LocalProviderConfig = z.infer<typeof LocalProviderSchema>;
 export type MaskingConfig = z.infer<typeof MaskingSchema>;
 export type SecretsDetectionConfig = z.infer<typeof SecretsDetectionSchema>;
+export type ServerConfig = z.infer<typeof ServerSchema>;
 
 /**
  * Replaces ${VAR} and ${VAR:-default} patterns with environment variable values
index e4dd8d071fe6cfb616b0eece30a11530dc10fe8b..1c948fd7acae69d74f254c0ffdd974b72a3306fe 100644 (file)
@@ -1,2 +1 @@
-export const REQUEST_TIMEOUT_MS = 120_000;
 export const HEALTH_CHECK_TIMEOUT_MS = 5_000;
index f73b69ebc4d9fa9d0117010ebc0018eef4eb1df8..41c095c4ec6f04de50794f7ce86459cb9257f2c9 100644 (file)
@@ -2,8 +2,7 @@
  * Anthropic client - simple functions for Anthropic Messages API
  */
 
-import type { AnthropicProviderConfig } from "../../config";
-import { REQUEST_TIMEOUT_MS } from "../../constants/timeouts";
+import { type AnthropicProviderConfig, getConfig } from "../../config";
 import { ProviderError } from "../errors";
 import type { AnthropicRequest, AnthropicResponse } from "./types";
 
@@ -68,11 +67,12 @@ export async function callAnthropic(
     headers["anthropic-beta"] = clientHeaders.beta;
   }
 
+  const timeoutMs = getConfig().server.request_timeout * 1000;
   const response = await fetch(`${baseUrl}/v1/messages`, {
     method: "POST",
     headers,
     body: JSON.stringify(request),
-    signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
+    signal: timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined,
   });
 
   if (!response.ok) {
index 6670655596b1eed364cf98b5869e8aa33b502ea7..cb206249065bc49717c385f0545791181409b04c 100644 (file)
@@ -3,8 +3,8 @@
  * Used in route mode for PII-containing requests (no masking needed)
  */
 
-import type { LocalProviderConfig } from "../config";
-import { HEALTH_CHECK_TIMEOUT_MS, REQUEST_TIMEOUT_MS } from "../constants/timeouts";
+import { getConfig, type LocalProviderConfig } from "../config";
+import { HEALTH_CHECK_TIMEOUT_MS } from "../constants/timeouts";
 import type { AnthropicResult } from "./anthropic/client";
 import type { AnthropicRequest, AnthropicResponse } from "./anthropic/types";
 import { ProviderError, type ProviderResult } from "./openai/client";
@@ -27,12 +27,13 @@ export async function callLocal(
   }
 
   const isStreaming = request.stream ?? false;
+  const timeoutMs = getConfig().server.request_timeout * 1000;
 
   const response = await fetch(endpoint, {
     method: "POST",
     headers,
     body: JSON.stringify({ ...request, model: config.model, stream: isStreaming }),
-    signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
+    signal: timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined,
   });
 
   if (!response.ok) {
@@ -70,12 +71,13 @@ export async function callLocalAnthropic(
   }
 
   const isStreaming = request.stream ?? false;
+  const timeoutMs = getConfig().server.request_timeout * 1000;
 
   const response = await fetch(endpoint, {
     method: "POST",
     headers,
     body: JSON.stringify({ ...request, model: config.model, stream: isStreaming }),
-    signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
+    signal: timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined,
   });
 
   if (!response.ok) {
index 9768abc9d308240428288971a0a9dc79a97bb428..902ee08c230a4b203c0eba710abeabde99ea7804 100644 (file)
@@ -2,8 +2,7 @@
  * OpenAI client - simple functions for OpenAI API
  */
 
-import type { OpenAIProviderConfig } from "../../config";
-import { REQUEST_TIMEOUT_MS } from "../../constants/timeouts";
+import { getConfig, type OpenAIProviderConfig } from "../../config";
 import { ProviderError } from "../errors";
 import type { OpenAIRequest, OpenAIResponse } from "./types";
 
@@ -66,11 +65,12 @@ export async function callOpenAI(
     delete body.max_tokens;
   }
 
+  const timeoutMs = getConfig().server.request_timeout * 1000;
   const response = await fetch(endpoint, {
     method: "POST",
     headers,
     body: JSON.stringify(body),
-    signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
+    signal: timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined,
   });
 
   if (!response.ok) {
git clone https://git.99rst.org/PROJECT