type StreamReadinessBody = Record | null | undefined; export type StreamReadinessPolicyInput = { baseTimeoutMs: number; provider?: string | null; model?: string | null; body?: StreamReadinessBody; maxTimeoutMs?: number; }; export type StreamReadinessPolicyResult = { timeoutMs: number; baseTimeoutMs: number; reasons: string[]; }; const DEFAULT_MAX_TIMEOUT_MS = 180_000; const LARGE_ITEM_THRESHOLD = 150; const VERY_LARGE_ITEM_THRESHOLD = 400; const TOOL_HEAVY_THRESHOLD = 15; const LARGE_CHAR_THRESHOLD = 250_000; const VERY_LARGE_CHAR_THRESHOLD = 750_000; function countArrayField(body: StreamReadinessBody, field: "input" | "messages" | "tools"): number { const value = body?.[field]; return Array.isArray(value) ? value.length : 0; } function estimateBodyChars(body: StreamReadinessBody): number { if (!body) return 0; try { return JSON.stringify(body).length; } catch { return 0; } } function isCodexGpt5x(provider?: string | null, model?: string | null): boolean { const normalizedProvider = (provider || "").toLowerCase(); const normalizedModel = (model || "").toLowerCase(); // Match the gpt-5.x family (gpt-5, gpt-5.1, gpt-5.5, ...) on the codex provider. return normalizedProvider === "codex" && /gpt-5(\.\d+)?/.test(normalizedModel); } /** * High-reasoning Codex GPT-5.x targets do a cold, expensive reasoning warm-up * (~78s TTFB) even for small prompts. Detect "high" reasoning effort either from * the model alias suffix (`...-high`) or from the request body's reasoning effort * field (OpenAI `reasoning_effort` or Responses API `reasoning.effort`). */ function isHighReasoningEffort( model: string | null | undefined, body: StreamReadinessBody ): boolean { const normalizedModel = (model || "").toLowerCase(); if (/-high\b/.test(normalizedModel) || normalizedModel.endsWith("-high")) return true; const effort = (() => { const direct = body?.["reasoning_effort"]; if (typeof direct === "string") return direct; const reasoning = body?.["reasoning"]; if (reasoning && typeof reasoning === "object") { const nested = (reasoning as Record)["effort"]; if (typeof nested === "string") return nested; } return ""; })(); return effort.toLowerCase() === "high"; } export function resolveStreamReadinessTimeout( input: StreamReadinessPolicyInput ): StreamReadinessPolicyResult { const baseTimeoutMs = Math.max(0, Math.floor(input.baseTimeoutMs || 0)); if (baseTimeoutMs <= 0) { return { timeoutMs: baseTimeoutMs, baseTimeoutMs, reasons: ["disabled"] }; } const maxTimeoutMs = Math.max(baseTimeoutMs, input.maxTimeoutMs ?? DEFAULT_MAX_TIMEOUT_MS); const reasons: string[] = []; let timeoutMs = baseTimeoutMs; const inputCount = countArrayField(input.body, "input"); const messageCount = countArrayField(input.body, "messages"); const itemCount = Math.max(inputCount, messageCount); const toolCount = countArrayField(input.body, "tools"); const estimatedChars = estimateBodyChars(input.body); const codexGpt5x = isCodexGpt5x(input.provider, input.model); const codexHighReasoning = codexGpt5x && isHighReasoningEffort(input.model, input.body); if (itemCount > VERY_LARGE_ITEM_THRESHOLD) { timeoutMs += 45_000; reasons.push("very_large_history"); } else if (itemCount > LARGE_ITEM_THRESHOLD) { timeoutMs += 20_000; reasons.push("large_history"); } if (toolCount >= TOOL_HEAVY_THRESHOLD) { timeoutMs += 15_000; reasons.push("tool_heavy"); } if (estimatedChars > VERY_LARGE_CHAR_THRESHOLD) { timeoutMs += 45_000; reasons.push("very_large_payload"); } else if (estimatedChars > LARGE_CHAR_THRESHOLD) { timeoutMs += 20_000; reasons.push("large_payload"); } // #3825: high-reasoning Codex GPT-5.x cold-starts at ~78s TTFB even for tiny // prompts, so the +30s readiness budget must fire UNCONDITIONALLY for the // high-effort case — the 80s base alone produced intermittent 504s at the // readiness window. The legacy large-request bump still applies to non-high // codex GPT-5.x requests (large history / tool-heavy). if (codexHighReasoning) { timeoutMs += 30_000; reasons.push("codex_gpt_5_5_high_reasoning"); } else if ( codexGpt5x && (itemCount > LARGE_ITEM_THRESHOLD || toolCount >= TOOL_HEAVY_THRESHOLD) ) { timeoutMs += 30_000; reasons.push("codex_gpt_5_5_large_responses"); } timeoutMs = Math.min(timeoutMs, maxTimeoutMs); if (timeoutMs === baseTimeoutMs) reasons.push("base"); return { timeoutMs, baseTimeoutMs, reasons }; }