/** * BA-18 — shared request-timeout helper for the http(s)-based providers (Anthropic, OpenAI, * Gemini, Ollama). They all build a `http.ClientRequest` with only `req.on('error')` wired, so a * socket the server silently dropped — or a response that never starts — was bounded only by the * OS TCP timeout (~2h on Linux). That presents to the caller as a hang, not a failure, so every * retry/casualty policy above it is inert. This adds a finite, configurable idle bound in one * place so the four providers cannot drift. */ export const DEFAULT_TIMEOUT_MS: 600000; /** * Resolve an effective timeout-shaped ms bound. A per-call value overrides the instance default, but * `null` and `undefined` BOTH mean "inherit" — so a per-call `null` never shadows an instance-level * disable. The explicit opt-out idiom is `0` or `Infinity` → returns 0 (no bound). When the knob is * UNSET on both, returns `defaultMs` (the idle bound's 10 min, or 0/disabled for the deadline). * * The disable-edge (finding-3, and the BA-4/5/6 optimistic-rounding family) is the load-bearing part: * a config MISTAKE — a NaN / negative / non-numeric value the caller EXPLICITLY set (e.g. * `Number(process.env.X)` on an unset var, or a string `'30s'`) — must never SILENTLY remove a bound * the caller evidently tried to set. Two cases, decided by whether the knob has a safe default: * - `defaultMs > 0` (the BA-18 idle bound): fail SAFE to that real default (a garbage idle value * keeps the 10-min safety net — byte-identical to the shipped BA-18 behaviour). * - `defaultMs === 0` (the BA-19 deadline, disabled-by-design): there is NO safe bound to fall back * to, so silently returning 0 would reintroduce the very hang the deadline exists to prevent * (BA-19 review finding 1). Fail LOUD instead — throw a {@link ValidationError} so the config * mistake surfaces immediately, rather than running unbounded for hours. * NOTE: an UNSET deadline (null/undefined) is legitimate and still returns 0 — only an explicitly-set * garbage value throws. * @param {number|undefined|null} instanceTimeout * @param {number|undefined|null} [callTimeout] * @param {number} [defaultMs=DEFAULT_TIMEOUT_MS] - value returned when the knob is unset (or garbage, if >0) * @param {string} [name='timeoutMs'] - knob name for the throw message * @returns {number} a finite positive ms bound, or 0 to disable * @throws {ValidationError} when the value is explicitly set but garbage AND `defaultMs` is 0 (no safe fallback) */ export function resolveTimeoutMs(instanceTimeout: number | undefined | null, callTimeout?: number | undefined | null, defaultMs?: number, name?: string): number; /** * Bound an in-flight ClientRequest on socket INACTIVITY. On timeout, destroy the request with a * retryable {@link TimeoutError} (`code: 'ETIMEDOUT'`) — `DEFAULT_RETRY_ON` classifies that as * transient, and the provider's own `req.on('error', reject)` turns it into a rejected promise, so * the caller regains control instead of hanging. Idle semantics (via `req.setTimeout`): the timer * resets on any socket activity, so a slow-but-streaming response is NOT killed — only a silent or * never-answering socket trips it. A `timeoutMs` of 0 is a no-op (bound disabled). * @param {import('http').ClientRequest} req * @param {number} timeoutMs - resolved bound; 0 disables * @param {string} providerName - for the error message (e.g. 'AnthropicProvider') */ export function applyRequestTimeout(req: import("http").ClientRequest, timeoutMs: number, providerName: string): void; /** * BA-19 — bound an in-flight ClientRequest on TOTAL call duration, beside {@link applyRequestTimeout}. * The idle bound (`req.setTimeout`) resets on ANY socket activity, so a response that trickles a byte * forever (a "zombie stream") never trips it — bytes keep arriving while the response never completes, * and the call hangs until the OS TCP timeout (~4.5h observed). This adds an absolute, non-resetting * wall-clock ceiling: a plain `setTimeout` that destroys the request whether or not the socket is * active. On trip, the request is destroyed with a TERMINAL {@link TimeoutError} — distinct * `code: 'EDEADLINE'` and `context.bound: 'deadline'` (so a consumer routing governance stops vs * transport casualties can tell which timer fired), and `retryable: false` because a deadline is a * HARD ceiling the caller set to STOP: auto-retrying would re-spend up to another full `deadlineMs` * of tokens/budget. Disabled-by-design (a deliberately long single call is legitimate); a * `deadlineMs` of 0 is a no-op. When both bounds are armed and `timeoutMs < deadlineMs`, a silent * socket trips the idle bound first; only a still-active-but-never-completing stream reaches the * deadline. The timer is unref'd (never keeps the event loop alive) and cleared when the request * closes (no dangling handle, no late destroy of a settled request). * @param {import('http').ClientRequest} req * @param {number} deadlineMs - resolved bound; 0 disables * @param {string} providerName - for the error message (e.g. 'AnthropicProvider') */ export function applyRequestDeadline(req: import("http").ClientRequest, deadlineMs: number, providerName: string): void; /** * Wire ALL request bounds onto a ClientRequest in one call — the single seam each provider's * `_request` uses, so the idle (BA-18) + deadline (BA-19) wiring is not copy-pasted at four call * sites (BA-19 review finding 2). A future third bound is added HERE once, not at every provider. * Each individual bound is a no-op when its ms value is 0/absent, so an unset knob costs nothing. * @param {import('http').ClientRequest} req * @param {{ timeoutMs?: number, deadlineMs?: number }} bounds - resolved ms bounds; 0/absent disables each * @param {string} providerName - for error messages (e.g. 'AnthropicProvider') */ export function applyRequestBounds(req: import("http").ClientRequest, bounds: { timeoutMs?: number; deadlineMs?: number; }, providerName: string): void; /** * BA-25 — guarantee the response promise SETTLES when the body is cut after headers. The provider * `_request` handlers wired only `res 'data'` + `res 'end'` (and `req 'error'`), so a socket the * server aborts or closes AFTER sending headers but BEFORE 'end' fired neither resolved nor rejected: * 'end' never came, and the BA-18 idle timer can't rescue it because the socket is already dead (no * further activity to time out against). The process then drains with an unsettled top-level await. * * Three terminal response events are the miss: `res 'aborted'` (peer reset mid-body), `res 'error'` * (stream error), and `res 'close'` WITHOUT a prior 'end' (clean-looking FIN before the body * completed). Each rejects with a RETRYABLE transport-class `ProviderError` (no HTTP status → the * status-derived retryability can't classify it, so `retryable:true` is explicit) so a wired * `Retry`/one-retry ladder (which keys on `err.retryable === true`) sees it instead of hanging. * * Returns `markEnded` — the 'end' handler MUST call it (before its own resolve/reject) so a normal * 'close' firing after 'end' does not spuriously reject an already-settled promise. Extra rejects * after settle are no-ops (Promise semantics), so ordering is safe either way; `markEnded` only * suppresses the benign post-'end' 'close'. * @param {import('http').IncomingMessage} res * @param {(err: Error) => void} reject - the _request Promise's reject * @param {string} providerName - for the error message (e.g. 'OpenAIProvider') * @returns {{ markEnded: () => void }} */ export function guardResponseSettles(res: import("http").IncomingMessage, reject: (err: Error) => void, providerName: string): { markEnded: () => void; };