diff --git a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts index 4309423..efbc58e 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts @@ -158,6 +158,12 @@ export interface LlmCallOpts { stream?: boolean; /** Optional AbortSignal to cancel the API request */ signal?: AbortSignal; + /** + * Per-request timeout in ms. Falls back to the client-level default + * (60s) when omitted. Vision/image analysis passes a longer budget here + * so a single large-image call isn't killed early by the shared default. + */ + timeout?: number; } /** @@ -242,6 +248,7 @@ export async function llmChat( ) => { const response = await client.chat.completions.create(currentParams, { signal, + ...(opts.timeout ? { timeout: opts.timeout } : {}), }); if (currentParams.stream) { let content = ""; @@ -352,6 +359,7 @@ export async function llmVision( top_p: 0.9, retries: 0, stream: true, // router always streams SSE; non-stream waits for full body and times out + timeout: config.AI_LLM_VISION_ANALYSIS_TIMEOUT_MS ?? 60_000, }); if (!completion) return null; diff --git a/services/discord-gateway/src/shared/config/index.ts b/services/discord-gateway/src/shared/config/index.ts index 1659809..0a9f7f8 100644 --- a/services/discord-gateway/src/shared/config/index.ts +++ b/services/discord-gateway/src/shared/config/index.ts @@ -189,6 +189,15 @@ export const configSchema = z .int() .positive() .default(60000), + // Standalone image/sticker/emoji vision analysis (analyzeSingleMediaImage + // → llmVision → llmChat). Decoupled from the media *batch* timeout above so + // a single vision call can be tuned independently. 1 minute by default — + // vision models (especially behind a router) need headroom for large images. + AI_LLM_VISION_ANALYSIS_TIMEOUT_MS: z.coerce + .number() + .int() + .positive() + .default(60000), // Text-only moderation batches are cheaper than media (no downloads / // vision pre-pass), so they get their own (shorter) timeout instead of // being tied to the media budget.