{"version":3,"file":"ai-model/service-caller/request-timeout.mjs","sources":["../../../../src/ai-model/service-caller/request-timeout.ts"],"sourcesContent":["import type { IModelConfig } from '@midscene/shared/env';\n\n/**\n * Default hard timeout (ms) applied to every AI HTTP call.\n *\n * We need an end-to-end timeout for the whole request lifecycle, not just the\n * time until response headers arrive. Some providers can return headers\n * quickly and then stall while the body is still being read.\n *\n * Override per intent via `MIDSCENE_MODEL_TIMEOUT`,\n * `MIDSCENE_INSIGHT_MODEL_TIMEOUT`, or `MIDSCENE_PLANNING_MODEL_TIMEOUT`.\n * Set the env var (or `modelConfig.timeout`) to `0` to disable the hard\n * timeout entirely; only a caller-provided `abortSignal` will cancel the\n * request in that case.\n */\nexport const DEFAULT_AI_CALL_TIMEOUT_MS = 180_000;\n\n/** Identifying code set on the AbortError raised by our hard timeout. */\nexport const AI_CALL_HARD_TIMEOUT_CODE = 'AI_CALL_HARD_TIMEOUT';\n\n/**\n * Resolve the hard request timeout for an AI call.\n * Returns `null` when the user explicitly opted out (`timeout === 0`).\n */\nexport function resolveEffectiveTimeoutMs(\n  modelConfig: Pick<IModelConfig, 'timeout'>,\n): number | null {\n  const { timeout } = modelConfig;\n  if (typeof timeout !== 'number') return DEFAULT_AI_CALL_TIMEOUT_MS;\n  if (timeout <= 0) return null;\n  return timeout;\n}\n\n/**\n * True if the error was raised by our hard-timeout AbortSignal (vs any other\n * abort/network/HTTP error). Used to drive observability without having to\n * string-match the message.\n */\nexport function isHardTimeoutError(err: unknown): boolean {\n  if (!err || typeof err !== 'object') return false;\n  const code = (err as { code?: unknown }).code;\n  if (code === AI_CALL_HARD_TIMEOUT_CODE) return true;\n  const cause = (err as { cause?: unknown }).cause;\n  if (\n    cause &&\n    typeof cause === 'object' &&\n    (cause as { code?: unknown }).code === AI_CALL_HARD_TIMEOUT_CODE\n  ) {\n    return true;\n  }\n  return false;\n}\n\n// Wires a hard timeout into the abort signal passed to fetch so the request\n// is actually cancelled even if the provider/client timeout only covers part\n// of the request. Honours any abortSignal supplied by the caller. Passing\n// `null` for `timeoutMs` disables the hard timeout and only forwards the user\n// signal.\nexport function buildRequestAbortSignal(\n  timeoutMs: number | null,\n  userSignal?: AbortSignal,\n): { signal: AbortSignal; cleanup: () => void } {\n  const controller = new AbortController();\n\n  if (userSignal?.aborted) {\n    controller.abort(userSignal.reason);\n    return { signal: controller.signal, cleanup: () => {} };\n  }\n\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  if (timeoutMs !== null) {\n    timer = setTimeout(() => {\n      const err = new Error(\n        `AI call hard timeout after ${timeoutMs}ms (full request time exceeded)`,\n      ) as Error & { code?: string };\n      err.code = AI_CALL_HARD_TIMEOUT_CODE;\n      controller.abort(err);\n    }, timeoutMs);\n    if (typeof (timer as { unref?: () => void }).unref === 'function') {\n      (timer as { unref: () => void }).unref();\n    }\n  }\n\n  const onUserAbort = userSignal\n    ? () => controller.abort(userSignal.reason)\n    : undefined;\n  if (userSignal && onUserAbort) {\n    userSignal.addEventListener('abort', onUserAbort, { once: true });\n  }\n\n  return {\n    signal: controller.signal,\n    cleanup: () => {\n      if (timer) clearTimeout(timer);\n      if (userSignal && onUserAbort) {\n        userSignal.removeEventListener('abort', onUserAbort);\n      }\n    },\n  };\n}\n"],"names":["DEFAULT_AI_CALL_TIMEOUT_MS","AI_CALL_HARD_TIMEOUT_CODE","resolveEffectiveTimeoutMs","modelConfig","timeout","isHardTimeoutError","err","code","cause","buildRequestAbortSignal","timeoutMs","userSignal","controller","AbortController","timer","setTimeout","Error","onUserAbort","undefined","clearTimeout"],"mappings":"AAeO,MAAMA,6BAA6B;AAGnC,MAAMC,4BAA4B;AAMlC,SAASC,0BACdC,WAA0C;IAE1C,MAAM,EAAEC,OAAO,EAAE,GAAGD;IACpB,IAAI,AAAmB,YAAnB,OAAOC,SAAsB,OAAOJ;IACxC,IAAII,WAAW,GAAG,OAAO;IACzB,OAAOA;AACT;AAOO,SAASC,mBAAmBC,GAAY;IAC7C,IAAI,CAACA,OAAO,AAAe,YAAf,OAAOA,KAAkB,OAAO;IAC5C,MAAMC,OAAQD,IAA2B,IAAI;IAC7C,IAAIC,SAASN,2BAA2B,OAAO;IAC/C,MAAMO,QAASF,IAA4B,KAAK;IAChD,IACEE,SACA,AAAiB,YAAjB,OAAOA,SACNA,MAA6B,IAAI,KAAKP,2BAEvC,OAAO;IAET,OAAO;AACT;AAOO,SAASQ,wBACdC,SAAwB,EACxBC,UAAwB;IAExB,MAAMC,aAAa,IAAIC;IAEvB,IAAIF,YAAY,SAAS;QACvBC,WAAW,KAAK,CAACD,WAAW,MAAM;QAClC,OAAO;YAAE,QAAQC,WAAW,MAAM;YAAE,SAAS,KAAO;QAAE;IACxD;IAEA,IAAIE;IACJ,IAAIJ,AAAc,SAAdA,WAAoB;QACtBI,QAAQC,WAAW;YACjB,MAAMT,MAAM,IAAIU,MACd,CAAC,2BAA2B,EAAEN,UAAU,+BAA+B,CAAC;YAE1EJ,IAAI,IAAI,GAAGL;YACXW,WAAW,KAAK,CAACN;QACnB,GAAGI;QACH,IAAI,AAAmD,cAAnD,OAAQI,MAAiC,KAAK,EAC/CA,MAAgC,KAAK;IAE1C;IAEA,MAAMG,cAAcN,aAChB,IAAMC,WAAW,KAAK,CAACD,WAAW,MAAM,IACxCO;IACJ,IAAIP,cAAcM,aAChBN,WAAW,gBAAgB,CAAC,SAASM,aAAa;QAAE,MAAM;IAAK;IAGjE,OAAO;QACL,QAAQL,WAAW,MAAM;QACzB,SAAS;YACP,IAAIE,OAAOK,aAAaL;YACxB,IAAIH,cAAcM,aAChBN,WAAW,mBAAmB,CAAC,SAASM;QAE5C;IACF;AACF"}