@midscene/core 1.12.8 → 1.12.9-beta-20260917120835.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/es/agent/utils.mjs +1 -1
  2. package/dist/es/ai-model/service-caller/call-ai.mjs +151 -40
  3. package/dist/es/ai-model/service-caller/call-ai.mjs.map +1 -1
  4. package/dist/es/ai-model/service-caller/codex/call-codex.mjs +26 -14
  5. package/dist/es/ai-model/service-caller/codex/call-codex.mjs.map +1 -1
  6. package/dist/es/ai-model/service-caller/codex/codex-app-server.mjs +12 -16
  7. package/dist/es/ai-model/service-caller/codex/codex-app-server.mjs.map +1 -1
  8. package/dist/es/ai-model/service-caller/index.mjs +3 -3
  9. package/dist/es/ai-model/service-caller/model-call.mjs +49 -0
  10. package/dist/es/ai-model/service-caller/model-call.mjs.map +1 -0
  11. package/dist/es/ai-model/service-caller/openai/call-openai.mjs +69 -0
  12. package/dist/es/ai-model/service-caller/openai/call-openai.mjs.map +1 -0
  13. package/dist/es/ai-model/service-caller/openai/chat-completion/chat-completion.mjs +50 -0
  14. package/dist/es/ai-model/service-caller/openai/chat-completion/chat-completion.mjs.map +1 -0
  15. package/dist/es/ai-model/service-caller/openai/chat-completion/non-stream.mjs +37 -0
  16. package/dist/es/ai-model/service-caller/openai/chat-completion/non-stream.mjs.map +1 -0
  17. package/dist/es/ai-model/service-caller/openai/chat-completion/stream.mjs +78 -0
  18. package/dist/es/ai-model/service-caller/openai/chat-completion/stream.mjs.map +1 -0
  19. package/dist/es/ai-model/service-caller/openai/chat-completion/utils.mjs +15 -0
  20. package/dist/es/ai-model/service-caller/openai/chat-completion/utils.mjs.map +1 -0
  21. package/dist/es/ai-model/service-caller/{openai-client.mjs → openai/openai-client.mjs} +9 -21
  22. package/dist/es/ai-model/service-caller/openai/openai-client.mjs.map +1 -0
  23. package/dist/es/ai-model/service-caller/{openai-error.mjs → openai/openai-request-context.mjs} +14 -46
  24. package/dist/es/ai-model/service-caller/openai/openai-request-context.mjs.map +1 -0
  25. package/dist/es/ai-model/service-caller/proxy/index.mjs +2 -0
  26. package/dist/es/ai-model/service-caller/proxy/proxy-deps.node.mjs.map +1 -0
  27. package/dist/es/ai-model/service-caller/proxy/proxy-deps.stub.mjs.map +1 -0
  28. package/dist/es/ai-model/service-caller/{proxy.mjs → proxy/proxy.mjs} +2 -2
  29. package/dist/es/ai-model/service-caller/proxy/proxy.mjs.map +1 -0
  30. package/dist/es/ai-model/service-caller/request-timeout.mjs +59 -18
  31. package/dist/es/ai-model/service-caller/request-timeout.mjs.map +1 -1
  32. package/dist/es/ai-model/service-caller/semantic-retry.mjs +9 -4
  33. package/dist/es/ai-model/service-caller/semantic-retry.mjs.map +1 -1
  34. package/dist/es/ai-model/service-caller/utils.mjs +25 -8
  35. package/dist/es/ai-model/service-caller/utils.mjs.map +1 -1
  36. package/dist/es/device/index.mjs +44 -21
  37. package/dist/es/device/index.mjs.map +1 -1
  38. package/dist/es/report-html-template.mjs +1 -1
  39. package/dist/es/types.mjs.map +1 -1
  40. package/dist/es/utils.mjs +1 -1
  41. package/dist/lib/agent/utils.js +1 -1
  42. package/dist/lib/ai-model/service-caller/call-ai.js +151 -46
  43. package/dist/lib/ai-model/service-caller/call-ai.js.map +1 -1
  44. package/dist/lib/ai-model/service-caller/codex/call-codex.js +30 -15
  45. package/dist/lib/ai-model/service-caller/codex/call-codex.js.map +1 -1
  46. package/dist/lib/ai-model/service-caller/codex/codex-app-server.js +12 -16
  47. package/dist/lib/ai-model/service-caller/codex/codex-app-server.js.map +1 -1
  48. package/dist/lib/ai-model/service-caller/index.js +7 -7
  49. package/dist/lib/ai-model/service-caller/model-call.js +89 -0
  50. package/dist/lib/ai-model/service-caller/model-call.js.map +1 -0
  51. package/dist/lib/ai-model/service-caller/openai/call-openai.js +103 -0
  52. package/dist/lib/ai-model/service-caller/openai/call-openai.js.map +1 -0
  53. package/dist/lib/ai-model/service-caller/openai/chat-completion/chat-completion.js +87 -0
  54. package/dist/lib/ai-model/service-caller/openai/chat-completion/chat-completion.js.map +1 -0
  55. package/dist/lib/ai-model/service-caller/openai/chat-completion/non-stream.js +71 -0
  56. package/dist/lib/ai-model/service-caller/openai/chat-completion/non-stream.js.map +1 -0
  57. package/dist/lib/ai-model/service-caller/openai/chat-completion/stream.js +112 -0
  58. package/dist/lib/ai-model/service-caller/openai/chat-completion/stream.js.map +1 -0
  59. package/dist/lib/ai-model/service-caller/openai/chat-completion/types.js.map +1 -0
  60. package/dist/lib/ai-model/service-caller/{chat-completion → openai/chat-completion}/utils.js +3 -29
  61. package/dist/lib/ai-model/service-caller/openai/chat-completion/utils.js.map +1 -0
  62. package/dist/lib/ai-model/service-caller/{openai-client.js → openai/openai-client.js} +9 -21
  63. package/dist/lib/ai-model/service-caller/openai/openai-client.js.map +1 -0
  64. package/dist/lib/ai-model/service-caller/{openai-error.js → openai/openai-request-context.js} +14 -46
  65. package/dist/lib/ai-model/service-caller/openai/openai-request-context.js.map +1 -0
  66. package/dist/lib/ai-model/service-caller/proxy/index.js +38 -0
  67. package/dist/lib/ai-model/service-caller/proxy/index.js.map +1 -0
  68. package/dist/lib/ai-model/service-caller/proxy/proxy-deps.node.js.map +1 -0
  69. package/dist/lib/ai-model/service-caller/proxy/proxy-deps.stub.js.map +1 -0
  70. package/dist/lib/ai-model/service-caller/{proxy.js → proxy/proxy.js} +4 -4
  71. package/dist/lib/ai-model/service-caller/proxy/proxy.js.map +1 -0
  72. package/dist/lib/ai-model/service-caller/request-timeout.js +67 -20
  73. package/dist/lib/ai-model/service-caller/request-timeout.js.map +1 -1
  74. package/dist/lib/ai-model/service-caller/semantic-retry.js +9 -4
  75. package/dist/lib/ai-model/service-caller/semantic-retry.js.map +1 -1
  76. package/dist/lib/ai-model/service-caller/utils.js +30 -13
  77. package/dist/lib/ai-model/service-caller/utils.js.map +1 -1
  78. package/dist/lib/device/index.js +56 -21
  79. package/dist/lib/device/index.js.map +1 -1
  80. package/dist/lib/report-html-template.js +1 -1
  81. package/dist/lib/types.js.map +1 -1
  82. package/dist/lib/utils.js +1 -1
  83. package/dist/types/ai-model/service-caller/call-ai.d.ts +2 -24
  84. package/dist/types/ai-model/service-caller/codex/call-codex.d.ts +7 -1
  85. package/dist/types/ai-model/service-caller/codex/codex-app-server.d.ts +1 -2
  86. package/dist/types/ai-model/service-caller/index.d.ts +3 -3
  87. package/dist/types/ai-model/service-caller/model-call.d.ts +26 -0
  88. package/dist/types/ai-model/service-caller/openai/call-openai.d.ts +3 -0
  89. package/dist/types/ai-model/service-caller/openai/chat-completion/chat-completion.d.ts +10 -0
  90. package/dist/types/ai-model/service-caller/openai/chat-completion/non-stream.d.ts +3 -0
  91. package/dist/types/ai-model/service-caller/openai/chat-completion/stream.d.ts +3 -0
  92. package/dist/types/ai-model/service-caller/openai/chat-completion/types.d.ts +10 -0
  93. package/dist/types/ai-model/service-caller/openai/chat-completion/utils.d.ts +5 -0
  94. package/dist/types/ai-model/service-caller/openai/openai-client.d.ts +14 -0
  95. package/dist/types/ai-model/service-caller/openai/openai-request-context.d.ts +18 -0
  96. package/dist/types/ai-model/service-caller/proxy/index.d.ts +1 -0
  97. package/dist/types/ai-model/service-caller/{proxy.d.ts → proxy/proxy.d.ts} +1 -1
  98. package/dist/types/ai-model/service-caller/request-timeout.d.ts +14 -11
  99. package/dist/types/ai-model/service-caller/types.d.ts +19 -0
  100. package/dist/types/ai-model/service-caller/utils.d.ts +10 -4
  101. package/dist/types/device/index.d.ts +28 -6
  102. package/dist/types/types.d.ts +5 -0
  103. package/package.json +6 -6
  104. package/dist/es/ai-model/service-caller/call.mjs +0 -56
  105. package/dist/es/ai-model/service-caller/call.mjs.map +0 -1
  106. package/dist/es/ai-model/service-caller/chat-completion/call.mjs +0 -101
  107. package/dist/es/ai-model/service-caller/chat-completion/call.mjs.map +0 -1
  108. package/dist/es/ai-model/service-caller/chat-completion/non-stream.mjs +0 -80
  109. package/dist/es/ai-model/service-caller/chat-completion/non-stream.mjs.map +0 -1
  110. package/dist/es/ai-model/service-caller/chat-completion/stream.mjs +0 -87
  111. package/dist/es/ai-model/service-caller/chat-completion/stream.mjs.map +0 -1
  112. package/dist/es/ai-model/service-caller/chat-completion/utils.mjs +0 -35
  113. package/dist/es/ai-model/service-caller/chat-completion/utils.mjs.map +0 -1
  114. package/dist/es/ai-model/service-caller/openai-client.mjs.map +0 -1
  115. package/dist/es/ai-model/service-caller/openai-error.mjs.map +0 -1
  116. package/dist/es/ai-model/service-caller/proxy-deps.node.mjs.map +0 -1
  117. package/dist/es/ai-model/service-caller/proxy-deps.stub.mjs.map +0 -1
  118. package/dist/es/ai-model/service-caller/proxy.mjs.map +0 -1
  119. package/dist/lib/ai-model/service-caller/call.js +0 -90
  120. package/dist/lib/ai-model/service-caller/call.js.map +0 -1
  121. package/dist/lib/ai-model/service-caller/chat-completion/call.js +0 -135
  122. package/dist/lib/ai-model/service-caller/chat-completion/call.js.map +0 -1
  123. package/dist/lib/ai-model/service-caller/chat-completion/non-stream.js +0 -114
  124. package/dist/lib/ai-model/service-caller/chat-completion/non-stream.js.map +0 -1
  125. package/dist/lib/ai-model/service-caller/chat-completion/stream.js +0 -121
  126. package/dist/lib/ai-model/service-caller/chat-completion/stream.js.map +0 -1
  127. package/dist/lib/ai-model/service-caller/chat-completion/types.js.map +0 -1
  128. package/dist/lib/ai-model/service-caller/chat-completion/utils.js.map +0 -1
  129. package/dist/lib/ai-model/service-caller/openai-client.js.map +0 -1
  130. package/dist/lib/ai-model/service-caller/openai-error.js.map +0 -1
  131. package/dist/lib/ai-model/service-caller/proxy-deps.node.js.map +0 -1
  132. package/dist/lib/ai-model/service-caller/proxy-deps.stub.js.map +0 -1
  133. package/dist/lib/ai-model/service-caller/proxy.js.map +0 -1
  134. package/dist/types/ai-model/service-caller/call.d.ts +0 -4
  135. package/dist/types/ai-model/service-caller/chat-completion/call.d.ts +0 -2
  136. package/dist/types/ai-model/service-caller/chat-completion/non-stream.d.ts +0 -2
  137. package/dist/types/ai-model/service-caller/chat-completion/stream.d.ts +0 -2
  138. package/dist/types/ai-model/service-caller/chat-completion/types.d.ts +0 -25
  139. package/dist/types/ai-model/service-caller/chat-completion/utils.d.ts +0 -12
  140. package/dist/types/ai-model/service-caller/openai-client.d.ts +0 -14
  141. package/dist/types/ai-model/service-caller/openai-error.d.ts +0 -26
  142. /package/dist/es/ai-model/service-caller/{chat-completion → openai/chat-completion}/types.mjs +0 -0
  143. /package/dist/es/ai-model/service-caller/{proxy-deps.node.mjs → proxy/proxy-deps.node.mjs} +0 -0
  144. /package/dist/es/ai-model/service-caller/{proxy-deps.stub.mjs → proxy/proxy-deps.stub.mjs} +0 -0
  145. /package/dist/lib/ai-model/service-caller/{chat-completion → openai/chat-completion}/types.js +0 -0
  146. /package/dist/lib/ai-model/service-caller/{proxy-deps.node.js → proxy/proxy-deps.node.js} +0 -0
  147. /package/dist/lib/ai-model/service-caller/{proxy-deps.stub.js → proxy/proxy-deps.stub.js} +0 -0
  148. /package/dist/types/ai-model/service-caller/{proxy-deps.node.d.ts → proxy/proxy-deps.node.d.ts} +0 -0
  149. /package/dist/types/ai-model/service-caller/{proxy-deps.stub.d.ts → proxy/proxy-deps.stub.d.ts} +0 -0
@@ -2,11 +2,9 @@ import { MIDSCENE_LANGFUSE_DEBUG, MIDSCENE_LANGSMITH_DEBUG, globalConfigManager
2
2
  import { getDebug } from "@midscene/shared/logger";
3
3
  import { ifInBrowser } from "@midscene/shared/utils";
4
4
  import openai_0 from "openai";
5
- import { getVersion } from "../../utils.mjs";
6
- import { wrapOpenAICompatibleFetch } from "./openai-error.mjs";
7
- import { createProxyAgent } from "./proxy.mjs";
8
- import { resolveEffectiveTimeoutMs } from "./request-timeout.mjs";
9
- const createAndWrapClient = async ({ openaiBaseURL, openaiApiKey, openaiExtraConfig, createOpenAIClient, effectiveTimeoutMs, proxyAgent, executionId, openAIErrorResponseContext })=>{
5
+ import { getVersion } from "../../../utils.mjs";
6
+ import { wrapOpenAICompatibleFetch } from "./openai-request-context.mjs";
7
+ const createAndWrapClient = async ({ openaiBaseURL, openaiApiKey, openaiExtraConfig, createOpenAIClient, effectiveTimeoutMs, proxyAgent, executionId, openAIRequestContext })=>{
10
8
  const warnClient = getDebug('ai:call', {
11
9
  console: true
12
10
  });
@@ -24,7 +22,7 @@ const createAndWrapClient = async ({ openaiBaseURL, openaiApiKey, openaiExtraCon
24
22
  'x-midscene-version': getVersion(),
25
23
  'x-midscene-execution-id': executionId
26
24
  },
27
- fetch: wrapOpenAICompatibleFetch(openAIErrorResponseContext),
25
+ fetch: wrapOpenAICompatibleFetch(openAIRequestContext),
28
26
  maxRetries: 0,
29
27
  ...null !== effectiveTimeoutMs ? {
30
28
  timeout: effectiveTimeoutMs
@@ -53,16 +51,9 @@ const createAndWrapClient = async ({ openaiBaseURL, openaiApiKey, openaiExtraCon
53
51
  }
54
52
  return openai;
55
53
  };
56
- async function createChatClient({ modelConfig, executionId, recordEvent }) {
57
- const { socksProxy, httpProxy, modelName, openaiBaseURL, openaiApiKey, openaiExtraConfig, modelDescription, modelFamily, createOpenAIClient, timeout } = modelConfig;
58
- const proxyAgent = await createProxyAgent({
59
- socksProxy,
60
- httpProxy
61
- });
62
- const effectiveTimeoutMs = resolveEffectiveTimeoutMs({
63
- timeout
64
- });
65
- const openAIErrorResponseContext = {
54
+ async function createChatClient({ modelConfig, proxyAgent, effectiveTimeoutMs, executionId, recordEvent }) {
55
+ const { openaiBaseURL, openaiApiKey, openaiExtraConfig, createOpenAIClient } = modelConfig;
56
+ const openAIRequestContext = {
66
57
  recordEvent
67
58
  };
68
59
  const openai = await createAndWrapClient({
@@ -73,14 +64,11 @@ async function createChatClient({ modelConfig, executionId, recordEvent }) {
73
64
  effectiveTimeoutMs,
74
65
  proxyAgent,
75
66
  executionId,
76
- openAIErrorResponseContext
67
+ openAIRequestContext
77
68
  });
78
69
  return {
79
70
  completion: openai.chat.completions,
80
- modelName,
81
- modelDescription,
82
- modelFamily,
83
- openAIErrorResponseContext
71
+ openAIRequestContext
84
72
  };
85
73
  }
86
74
  export { createChatClient };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-model/service-caller/openai/openai-client.mjs","sources":["../../../../../src/ai-model/service-caller/openai/openai-client.ts"],"sourcesContent":["import {\n type IModelConfig,\n MIDSCENE_LANGFUSE_DEBUG,\n MIDSCENE_LANGSMITH_DEBUG,\n globalConfigManager,\n} from '@midscene/shared/env';\nimport { getDebug } from '@midscene/shared/logger';\nimport { ifInBrowser } from '@midscene/shared/utils';\nimport OpenAI from 'openai';\nimport { getVersion } from '../../../utils';\nimport type { createProxyAgentIfNeeded } from '../proxy';\nimport {\n type OpenAIRequestContext,\n wrapOpenAICompatibleFetch,\n} from './openai-request-context';\n\nconst createAndWrapClient = async ({\n openaiBaseURL,\n openaiApiKey,\n openaiExtraConfig,\n createOpenAIClient,\n effectiveTimeoutMs,\n proxyAgent,\n executionId,\n openAIRequestContext,\n}: {\n openaiBaseURL: IModelConfig['openaiBaseURL'];\n openaiApiKey: IModelConfig['openaiApiKey'];\n openaiExtraConfig: IModelConfig['openaiExtraConfig'];\n createOpenAIClient: IModelConfig['createOpenAIClient'];\n effectiveTimeoutMs: number | null;\n proxyAgent: Awaited<ReturnType<typeof createProxyAgentIfNeeded>>;\n executionId: string;\n openAIRequestContext: OpenAIRequestContext;\n}): Promise<OpenAI> => {\n const warnClient = getDebug('ai:call', { console: true });\n\n const openAIOptions = {\n baseURL: openaiBaseURL,\n apiKey: openaiApiKey,\n // Use fetchOptions.dispatcher for fetch-based SDK instead of httpAgent\n // Note: Type assertion needed due to undici version mismatch between dependencies\n ...(proxyAgent ? { fetchOptions: { dispatcher: proxyAgent as any } } : {}),\n ...openaiExtraConfig,\n // Midscene exposes this setting through MIDSCENE_*_INIT_CONFIG_JSON, so\n // defaultHeaders is expected to be a plain JSON object rather than another\n // HeadersLike representation supported by the OpenAI SDK.\n defaultHeaders: {\n ...(openaiExtraConfig?.defaultHeaders as\n | Record<string, string>\n | undefined),\n // These Midscene-owned headers intentionally override user-supplied\n // headers with the same names.\n 'x-midscene-version': getVersion(),\n 'x-midscene-execution-id': executionId,\n },\n fetch: wrapOpenAICompatibleFetch(openAIRequestContext),\n // Midscene handles request retries in callAI(), so disable SDK-level retries\n // to avoid duplicate attempts and duplicated backoff latency.\n maxRetries: 0,\n // Disabling the Midscene hard timeout leaves the SDK timeout at its\n // configured value or default; it does not disable SDK/network timeouts.\n ...(effectiveTimeoutMs !== null ? { timeout: effectiveTimeoutMs } : {}),\n dangerouslyAllowBrowser: true,\n };\n\n const baseOpenAI = new OpenAI(openAIOptions);\n\n let openai: OpenAI = baseOpenAI;\n\n // LangSmith wrapper\n if (\n openai &&\n globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGSMITH_DEBUG)\n ) {\n if (ifInBrowser) {\n throw new Error('langsmith is not supported in browser');\n }\n warnClient('DEBUGGING MODE: langsmith wrapper enabled');\n // Use variable to prevent static analysis by bundlers\n const langsmithModule = 'langsmith/wrappers';\n const { wrapOpenAI } = await import(langsmithModule);\n openai = wrapOpenAI(openai);\n }\n\n // Langfuse wrapper\n if (\n openai &&\n globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGFUSE_DEBUG)\n ) {\n if (ifInBrowser) {\n throw new Error('langfuse is not supported in browser');\n }\n warnClient('DEBUGGING MODE: langfuse wrapper enabled');\n // Use variable to prevent static analysis by bundlers\n const langfuseModule = '@langfuse/openai';\n const { observeOpenAI } = await import(langfuseModule);\n openai = observeOpenAI(openai);\n }\n\n if (createOpenAIClient) {\n const wrappedClient = await createOpenAIClient(baseOpenAI, openAIOptions);\n\n if (wrappedClient) {\n openai = wrappedClient as OpenAI;\n }\n }\n\n return openai;\n};\n\nexport async function createChatClient({\n modelConfig,\n proxyAgent,\n effectiveTimeoutMs,\n executionId,\n recordEvent,\n}: {\n modelConfig: IModelConfig;\n proxyAgent: Awaited<ReturnType<typeof createProxyAgentIfNeeded>>;\n effectiveTimeoutMs: number | null;\n executionId: string;\n recordEvent?: (event: Record<string, unknown>) => void;\n}): Promise<{\n completion: OpenAI.Chat.Completions;\n openAIRequestContext: OpenAIRequestContext;\n}> {\n const { openaiBaseURL, openaiApiKey, openaiExtraConfig, createOpenAIClient } =\n modelConfig;\n\n const openAIRequestContext: OpenAIRequestContext = {\n recordEvent,\n };\n\n const openai = await createAndWrapClient({\n openaiBaseURL,\n openaiApiKey,\n openaiExtraConfig,\n createOpenAIClient,\n effectiveTimeoutMs,\n proxyAgent,\n executionId,\n openAIRequestContext,\n });\n\n return {\n completion: openai.chat.completions,\n openAIRequestContext,\n };\n}\n"],"names":["createAndWrapClient","openaiBaseURL","openaiApiKey","openaiExtraConfig","createOpenAIClient","effectiveTimeoutMs","proxyAgent","executionId","openAIRequestContext","warnClient","getDebug","openAIOptions","getVersion","wrapOpenAICompatibleFetch","baseOpenAI","OpenAI","openai","globalConfigManager","MIDSCENE_LANGSMITH_DEBUG","ifInBrowser","Error","langsmithModule","wrapOpenAI","MIDSCENE_LANGFUSE_DEBUG","langfuseModule","observeOpenAI","wrappedClient","createChatClient","modelConfig","recordEvent"],"mappings":";;;;;;AAgBA,MAAMA,sBAAsB,OAAO,EACjCC,aAAa,EACbC,YAAY,EACZC,iBAAiB,EACjBC,kBAAkB,EAClBC,kBAAkB,EAClBC,UAAU,EACVC,WAAW,EACXC,oBAAoB,EAUrB;IACC,MAAMC,aAAaC,SAAS,WAAW;QAAE,SAAS;IAAK;IAEvD,MAAMC,gBAAgB;QACpB,SAASV;QACT,QAAQC;QAGR,GAAII,aAAa;YAAE,cAAc;gBAAE,YAAYA;YAAkB;QAAE,IAAI,CAAC,CAAC;QACzE,GAAGH,iBAAiB;QAIpB,gBAAgB;YACd,GAAIA,mBAAmB,cAAc;YAKrC,sBAAsBS;YACtB,2BAA2BL;QAC7B;QACA,OAAOM,0BAA0BL;QAGjC,YAAY;QAGZ,GAAIH,AAAuB,SAAvBA,qBAA8B;YAAE,SAASA;QAAmB,IAAI,CAAC,CAAC;QACtE,yBAAyB;IAC3B;IAEA,MAAMS,aAAa,IAAIC,SAAOJ;IAE9B,IAAIK,SAAiBF;IAGrB,IACEE,UACAC,oBAAoB,qBAAqB,CAACC,2BAC1C;QACA,IAAIC,aACF,MAAM,IAAIC,MAAM;QAElBX,WAAW;QAEX,MAAMY,kBAAkB;QACxB,MAAM,EAAEC,UAAU,EAAE,GAAG,MAAM,MAAM,CAACD;QACpCL,SAASM,WAAWN;IACtB;IAGA,IACEA,UACAC,oBAAoB,qBAAqB,CAACM,0BAC1C;QACA,IAAIJ,aACF,MAAM,IAAIC,MAAM;QAElBX,WAAW;QAEX,MAAMe,iBAAiB;QACvB,MAAM,EAAEC,aAAa,EAAE,GAAG,MAAM,MAAM,CAACD;QACvCR,SAASS,cAAcT;IACzB;IAEA,IAAIZ,oBAAoB;QACtB,MAAMsB,gBAAgB,MAAMtB,mBAAmBU,YAAYH;QAE3D,IAAIe,eACFV,SAASU;IAEb;IAEA,OAAOV;AACT;AAEO,eAAeW,iBAAiB,EACrCC,WAAW,EACXtB,UAAU,EACVD,kBAAkB,EAClBE,WAAW,EACXsB,WAAW,EAOZ;IAIC,MAAM,EAAE5B,aAAa,EAAEC,YAAY,EAAEC,iBAAiB,EAAEC,kBAAkB,EAAE,GAC1EwB;IAEF,MAAMpB,uBAA6C;QACjDqB;IACF;IAEA,MAAMb,SAAS,MAAMhB,oBAAoB;QACvCC;QACAC;QACAC;QACAC;QACAC;QACAC;QACAC;QACAC;IACF;IAEA,OAAO;QACL,YAAYQ,OAAO,IAAI,CAAC,WAAW;QACnCR;IACF;AACF"}
@@ -44,14 +44,11 @@ function getDefaultFetch() {
44
44
  }
45
45
  function wrapOpenAICompatibleFetch(context) {
46
46
  const baseFetch = getDefaultFetch();
47
- let attempt = 0;
48
47
  return async (input, init)=>{
49
- attempt += 1;
50
48
  if (context.recordEvent) {
51
49
  const request = new Request(input, init);
52
50
  context.recordEvent({
53
51
  type: 'request',
54
- attempt,
55
52
  request: {
56
53
  url: request.url,
57
54
  method: request.method,
@@ -65,43 +62,27 @@ function wrapOpenAICompatibleFetch(context) {
65
62
  } catch (error) {
66
63
  const fetchErrorSummary = formatFetchErrorForReport(error);
67
64
  debugOpenAIFetch('OpenAI-compatible fetch failed', fetchErrorSummary);
68
- context.fetchErrors ??= [];
69
- context.fetchErrors.push({
70
- attempt,
71
- error: fetchErrorSummary
72
- });
65
+ context.fetchError = fetchErrorSummary;
73
66
  context.recordEvent?.({
74
67
  type: 'error',
75
- attempt,
76
68
  error: fetchErrorSummary
77
69
  });
78
70
  throw error;
79
71
  }
80
72
  const requestId = response.headers.get('x-request-id') ?? response.headers.get('x-model-request-id');
81
- if (requestId) {
82
- context.responseRequestIds ??= [];
83
- context.responseRequestIds.push({
84
- attempt,
85
- requestId,
86
- status: response.status,
87
- ok: response.ok
88
- });
89
- }
73
+ if (requestId) context.responseRequestId = {
74
+ requestId,
75
+ status: response.status,
76
+ ok: response.ok
77
+ };
90
78
  if (!response.ok) {
91
79
  const rawResponseBody = await response.clone().text().catch(()=>void 0);
92
- if (void 0 !== rawResponseBody) {
93
- context.rawResponseBodies ??= [];
94
- context.rawResponseBodies.push({
95
- attempt,
96
- body: rawResponseBody
97
- });
98
- }
80
+ context.rawResponseBody = rawResponseBody;
99
81
  }
100
82
  if (context.recordEvent) {
101
83
  const isStream = response.headers.get('content-type')?.includes('text/event-stream');
102
84
  const body = isStream ? void 0 : await response.clone().text().catch(()=>void 0);
103
85
  const httpResponse = {
104
- attempt,
105
86
  status: response.status,
106
87
  ok: response.ok,
107
88
  headers: headersToEntries(response.headers),
@@ -109,8 +90,7 @@ function wrapOpenAICompatibleFetch(context) {
109
90
  body
110
91
  }
111
92
  };
112
- context.httpResponses ??= [];
113
- context.httpResponses.push(httpResponse);
93
+ context.httpResponse = httpResponse;
114
94
  if (!response.ok) context.recordEvent({
115
95
  type: 'error',
116
96
  ...httpResponse
@@ -121,27 +101,15 @@ function wrapOpenAICompatibleFetch(context) {
121
101
  }
122
102
  function formatOpenAIAPIErrorDetails(_error, context) {
123
103
  const details = [];
124
- if (context.rawResponseBodies?.length === 1) details.push(`OpenAI raw error response body: ${truncateErrorResponseBody(context.rawResponseBodies[0].body)}`);
125
- else if (context.rawResponseBodies?.length) {
126
- const rawResponseBodyDetails = context.rawResponseBodies.map(({ attempt, body })=>`Attempt ${attempt}: ${truncateErrorResponseBody(body)}`).join('\n');
127
- details.push(`OpenAI raw error response bodies:\n${rawResponseBodyDetails}`);
128
- }
129
- const errorResponseRequestIds = context.responseRequestIds?.filter(({ ok })=>!ok);
130
- if (errorResponseRequestIds?.length === 1) {
131
- const { attempt, requestId, status } = errorResponseRequestIds[0];
132
- details.push(`OpenAI error response request ID (attempt ${attempt}, status ${status}): ${requestId}`);
133
- } else if (errorResponseRequestIds?.length) {
134
- const requestIdDetails = errorResponseRequestIds.map(({ attempt, requestId, status })=>`Attempt ${attempt} (status ${status}): ${requestId}`).join('\n');
135
- details.push(`OpenAI error response request IDs:\n${requestIdDetails}`);
136
- }
137
- if (context.fetchErrors?.length === 1) details.push(`OpenAI fetch error (attempt ${context.fetchErrors[0].attempt}): ${context.fetchErrors[0].error}`);
138
- else if (context.fetchErrors?.length) {
139
- const fetchErrorDetails = context.fetchErrors.map(({ attempt, error })=>`Attempt ${attempt}: ${error}`).join('\n');
140
- details.push(`OpenAI fetch errors:\n${fetchErrorDetails}`);
104
+ if (void 0 !== context.rawResponseBody) details.push(`OpenAI raw error response body: ${truncateErrorResponseBody(context.rawResponseBody)}`);
105
+ if (context.responseRequestId && !context.responseRequestId.ok) {
106
+ const { requestId, status } = context.responseRequestId;
107
+ details.push(`OpenAI error response request ID (status ${status}): ${requestId}`);
141
108
  }
109
+ if (void 0 !== context.fetchError) details.push(`OpenAI fetch error: ${context.fetchError}`);
142
110
  if (!details.length) return '';
143
111
  return `\n${details.join('\n')}`;
144
112
  }
145
113
  export { formatOpenAIAPIErrorDetails, wrapOpenAICompatibleFetch };
146
114
 
147
- //# sourceMappingURL=openai-error.mjs.map
115
+ //# sourceMappingURL=openai-request-context.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-model/service-caller/openai/openai-request-context.mjs","sources":["../../../../../src/ai-model/service-caller/openai/openai-request-context.ts"],"sourcesContent":["import { getDebug } from '@midscene/shared/logger';\n\nconst MAX_ERROR_RESPONSE_BODY_LENGTH = 4000;\nconst MAX_FETCH_ERROR_LENGTH = 4000;\n\nconst debugOpenAIFetch = getDebug('ai:call');\n\nexport interface OpenAIRequestContext {\n recordEvent?: (event: Record<string, unknown>) => void;\n responseRequestId?: {\n requestId: string;\n status: number;\n ok: boolean;\n };\n rawResponseBody?: string;\n fetchError?: string;\n httpResponse?: {\n status: number;\n ok: boolean;\n headers: Array<[string, string]>;\n body?: string;\n };\n}\n\nfunction headersToEntries(\n headers: HeadersInit | undefined,\n): Array<[string, string]> {\n const entries: Array<[string, string]> = [];\n new Headers(headers).forEach((value, key) => entries.push([key, value]));\n return entries;\n}\n\nfunction truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) {\n return text;\n }\n\n return `${text.slice(0, maxLength)}... [truncated, ${text.length} chars total]`;\n}\n\nfunction truncateErrorResponseBody(body: string): string {\n return truncateText(body, MAX_ERROR_RESPONSE_BODY_LENGTH);\n}\n\nfunction getErrorCode(error: unknown): string | undefined {\n if (!error || typeof error !== 'object') {\n return undefined;\n }\n\n const code = (error as { code?: unknown }).code;\n return typeof code === 'string' ? code : undefined;\n}\n\nfunction formatErrorSummary(error: unknown): string {\n if (error instanceof Error) {\n const code = getErrorCode(error);\n const codeText = code ? ` [${code}]` : '';\n return `${error.name}${codeText}: ${error.message}`;\n }\n\n return String(error);\n}\n\nfunction formatFetchErrorForReport(error: unknown): string {\n const details = [formatErrorSummary(error)];\n const cause =\n error && typeof error === 'object'\n ? (error as { cause?: unknown }).cause\n : undefined;\n\n if (cause !== undefined) {\n details.push(`Cause: ${formatErrorSummary(cause)}`);\n }\n\n return truncateText(details.join('\\n'), MAX_FETCH_ERROR_LENGTH);\n}\n\n// Mirrors OpenAI SDK's default fetch selection:\n// openai@6.3.0 src/client.ts sets `this.fetch = options.fetch ?? Shims.getDefaultFetch()`,\n// and src/internal/shims.ts resolves that default to global `fetch`.\nfunction getDefaultFetch(): typeof fetch {\n if (typeof globalThis.fetch === 'function') {\n return globalThis.fetch;\n }\n\n throw new Error(\n '`fetch` is not defined as a global; check that the runtime provides globalThis.fetch or polyfill it before creating the OpenAI client',\n );\n}\n\nexport function wrapOpenAICompatibleFetch(\n context: OpenAIRequestContext,\n): typeof fetch {\n const baseFetch = getDefaultFetch();\n\n return async (input, init) => {\n if (context.recordEvent) {\n // Midscene does not expose a custom fetch hook. The OpenAI SDK therefore\n // always invokes this wrapper with standard Fetch API input and init.\n const request = new Request(input, init);\n context.recordEvent({\n type: 'request',\n request: {\n url: request.url,\n method: request.method,\n body: await request\n .clone()\n .text()\n .catch(() => undefined),\n },\n });\n }\n let response: Response;\n try {\n response = await baseFetch(input, init);\n } catch (error) {\n const fetchErrorSummary = formatFetchErrorForReport(error);\n debugOpenAIFetch('OpenAI-compatible fetch failed', fetchErrorSummary);\n context.fetchError = fetchErrorSummary;\n context.recordEvent?.({\n type: 'error',\n error: fetchErrorSummary,\n });\n throw error;\n }\n\n const requestId =\n response.headers.get('x-request-id') ??\n response.headers.get('x-model-request-id');\n\n if (requestId) {\n context.responseRequestId = {\n requestId,\n status: response.status,\n ok: response.ok,\n };\n }\n\n if (!response.ok) {\n // OpenAI SDK only exposes the `error` field for JSON error responses.\n // Non-standard provider bodies like `{ err: 'xxx' }` would otherwise be\n // hidden from Midscene's final error message.\n const rawResponseBody = await response\n .clone()\n .text()\n .catch(() => undefined);\n\n context.rawResponseBody = rawResponseBody;\n }\n\n if (context.recordEvent) {\n const isStream = response.headers\n .get('content-type')\n ?.includes('text/event-stream');\n const body = isStream\n ? undefined\n : await response\n .clone()\n .text()\n .catch(() => undefined);\n const httpResponse = {\n status: response.status,\n ok: response.ok,\n headers: headersToEntries(response.headers),\n ...(body === undefined ? {} : { body }),\n };\n context.httpResponse = httpResponse;\n if (!response.ok) {\n context.recordEvent({ type: 'error', ...httpResponse });\n }\n }\n\n return response;\n };\n}\n\nexport function formatOpenAIAPIErrorDetails(\n _error: unknown,\n context: OpenAIRequestContext,\n): string {\n const details: string[] = [];\n\n if (context.rawResponseBody !== undefined) {\n details.push(\n `OpenAI raw error response body: ${truncateErrorResponseBody(context.rawResponseBody)}`,\n );\n }\n\n if (context.responseRequestId && !context.responseRequestId.ok) {\n const { requestId, status } = context.responseRequestId;\n details.push(\n `OpenAI error response request ID (status ${status}): ${requestId}`,\n );\n }\n\n if (context.fetchError !== undefined) {\n details.push(`OpenAI fetch error: ${context.fetchError}`);\n }\n\n if (!details.length) {\n return '';\n }\n\n return `\\n${details.join('\\n')}`;\n}\n"],"names":["MAX_ERROR_RESPONSE_BODY_LENGTH","MAX_FETCH_ERROR_LENGTH","debugOpenAIFetch","getDebug","headersToEntries","headers","entries","Headers","value","key","truncateText","text","maxLength","truncateErrorResponseBody","body","getErrorCode","error","code","undefined","formatErrorSummary","Error","codeText","String","formatFetchErrorForReport","details","cause","getDefaultFetch","globalThis","wrapOpenAICompatibleFetch","context","baseFetch","input","init","request","Request","response","fetchErrorSummary","requestId","rawResponseBody","isStream","httpResponse","formatOpenAIAPIErrorDetails","_error","status"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { createProxyAgentIfNeeded } from "./proxy.mjs";
2
+ export { createProxyAgentIfNeeded };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-model/service-caller/proxy/proxy-deps.node.mjs","sources":["../../../../../src/ai-model/service-caller/proxy/proxy-deps.node.ts"],"sourcesContent":["export function loadUndici(): Promise<typeof import('undici')> {\n return import('undici');\n}\n\nexport function loadFetchSocks(): Promise<typeof import('fetch-socks')> {\n return import('fetch-socks');\n}\n"],"names":["loadUndici","loadFetchSocks"],"mappings":"AAAO,SAASA;IACd,OAAO,MAAM,CAAC;AAChB;AAEO,SAASC;IACd,OAAO,MAAM,CAAC;AAChB"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-model/service-caller/proxy/proxy-deps.stub.mjs","sources":["../../../../../src/ai-model/service-caller/proxy/proxy-deps.stub.ts"],"sourcesContent":["function unavailable(): never {\n throw new Error('proxy dependencies are unavailable in browser builds');\n}\n\nexport function loadUndici(): Promise<typeof import('undici')> {\n return unavailable();\n}\n\nexport function loadFetchSocks(): Promise<typeof import('fetch-socks')> {\n return unavailable();\n}\n"],"names":["unavailable","Error","loadUndici","loadFetchSocks"],"mappings":"AAAA,SAASA;IACP,MAAM,IAAIC,MAAM;AAClB;AAEO,SAASC;IACd,OAAOF;AACT;AAEO,SAASG;IACd,OAAOH;AACT"}
@@ -12,7 +12,7 @@ const sanitizeProxyUrl = (url)=>{
12
12
  return url;
13
13
  }
14
14
  };
15
- const createProxyAgent = async ({ socksProxy, httpProxy })=>{
15
+ const createProxyAgentIfNeeded = async ({ socksProxy, httpProxy })=>{
16
16
  const warnProxy = getDebug('ai:call:proxy', {
17
17
  console: true
18
18
  });
@@ -61,6 +61,6 @@ const createProxyAgent = async ({ socksProxy, httpProxy })=>{
61
61
  }
62
62
  }
63
63
  };
64
- export { createProxyAgent };
64
+ export { createProxyAgentIfNeeded };
65
65
 
66
66
  //# sourceMappingURL=proxy.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-model/service-caller/proxy/proxy.mjs","sources":["../../../../../src/ai-model/service-caller/proxy/proxy.ts"],"sourcesContent":["import { getDebug } from '@midscene/shared/logger';\nimport { ifInBrowser } from '@midscene/shared/utils';\n\n// Helper function to sanitize proxy URL for logging (remove credentials)\n// Uses URL API instead of regex to avoid ReDoS vulnerabilities\nconst sanitizeProxyUrl = (url: string): string => {\n try {\n const parsed = new URL(url);\n if (parsed.username) {\n // Keep username for debugging, hide password for security\n parsed.password = '****';\n return parsed.href;\n }\n return url;\n } catch {\n // If URL parsing fails, return original URL (will be caught later)\n return url;\n }\n};\n\nexport const createProxyAgentIfNeeded = async ({\n socksProxy,\n httpProxy,\n}: {\n socksProxy: string | undefined;\n httpProxy: string | undefined;\n}): Promise<unknown> => {\n const warnProxy = getDebug('ai:call:proxy', { console: true });\n const debugProxy = getDebug('ai:call:proxy');\n\n if (httpProxy) {\n debugProxy('using http proxy', sanitizeProxyUrl(httpProxy));\n if (ifInBrowser) {\n warnProxy(\n 'HTTP proxy is configured but not supported in browser environment',\n );\n } else {\n const { loadUndici } = await import('#proxy-deps');\n const { ProxyAgent } = await loadUndici();\n const proxyAgent = new ProxyAgent({\n uri: httpProxy,\n // Note: authentication is handled via the URI (e.g., http://user:pass@proxy.com:8080)\n });\n return proxyAgent;\n }\n } else if (socksProxy) {\n debugProxy('using socks proxy', sanitizeProxyUrl(socksProxy));\n if (ifInBrowser) {\n warnProxy(\n 'SOCKS proxy is configured but not supported in browser environment',\n );\n } else {\n try {\n const { loadFetchSocks } = await import('#proxy-deps');\n const { socksDispatcher } = await loadFetchSocks();\n // Parse SOCKS proxy URL (e.g., socks5://127.0.0.1:1080)\n const proxyUrl = new URL(socksProxy);\n\n // Validate hostname\n if (!proxyUrl.hostname) {\n throw new Error('SOCKS proxy URL must include a valid hostname');\n }\n\n // Validate and parse port\n const port = Number.parseInt(proxyUrl.port, 10);\n if (!proxyUrl.port || Number.isNaN(port)) {\n throw new Error('SOCKS proxy URL must include a valid port');\n }\n\n // Parse SOCKS version from protocol\n const protocol = proxyUrl.protocol.replace(':', '');\n const socksType =\n protocol === 'socks4' ? 4 : protocol === 'socks5' ? 5 : 5;\n\n const proxyAgent = socksDispatcher({\n type: socksType,\n host: proxyUrl.hostname,\n port,\n ...(proxyUrl.username\n ? {\n userId: decodeURIComponent(proxyUrl.username),\n password: decodeURIComponent(proxyUrl.password || ''),\n }\n : {}),\n });\n debugProxy('socks proxy configured successfully', {\n type: socksType,\n host: proxyUrl.hostname,\n port: port,\n });\n return proxyAgent;\n } catch (error) {\n warnProxy('Failed to configure SOCKS proxy:', error);\n throw new Error(\n `Invalid SOCKS proxy URL: ${socksProxy}. Expected format: socks4://host:port, socks5://host:port, or with authentication: socks5://user:pass@host:port`,\n );\n }\n }\n }\n};\n"],"names":["sanitizeProxyUrl","url","parsed","URL","createProxyAgentIfNeeded","socksProxy","httpProxy","warnProxy","getDebug","debugProxy","ifInBrowser","loadUndici","ProxyAgent","proxyAgent","loadFetchSocks","socksDispatcher","proxyUrl","Error","port","Number","protocol","socksType","decodeURIComponent","error"],"mappings":";;AAKA,MAAMA,mBAAmB,CAACC;IACxB,IAAI;QACF,MAAMC,SAAS,IAAIC,IAAIF;QACvB,IAAIC,OAAO,QAAQ,EAAE;YAEnBA,OAAO,QAAQ,GAAG;YAClB,OAAOA,OAAO,IAAI;QACpB;QACA,OAAOD;IACT,EAAE,OAAM;QAEN,OAAOA;IACT;AACF;AAEO,MAAMG,2BAA2B,OAAO,EAC7CC,UAAU,EACVC,SAAS,EAIV;IACC,MAAMC,YAAYC,SAAS,iBAAiB;QAAE,SAAS;IAAK;IAC5D,MAAMC,aAAaD,SAAS;IAE5B,IAAIF,WAAW;QACbG,WAAW,oBAAoBT,iBAAiBM;QAChD,IAAII,aACFH,UACE;aAEG;YACL,MAAM,EAAEI,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC;YACpC,MAAM,EAAEC,UAAU,EAAE,GAAG,MAAMD;YAC7B,MAAME,aAAa,IAAID,WAAW;gBAChC,KAAKN;YAEP;YACA,OAAOO;QACT;IACF,OAAO,IAAIR,YAAY;QACrBI,WAAW,qBAAqBT,iBAAiBK;QACjD,IAAIK,aACFH,UACE;aAGF,IAAI;YACF,MAAM,EAAEO,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC;YACxC,MAAM,EAAEC,eAAe,EAAE,GAAG,MAAMD;YAElC,MAAME,WAAW,IAAIb,IAAIE;YAGzB,IAAI,CAACW,SAAS,QAAQ,EACpB,MAAM,IAAIC,MAAM;YAIlB,MAAMC,OAAOC,OAAO,QAAQ,CAACH,SAAS,IAAI,EAAE;YAC5C,IAAI,CAACA,SAAS,IAAI,IAAIG,OAAO,KAAK,CAACD,OACjC,MAAM,IAAID,MAAM;YAIlB,MAAMG,WAAWJ,SAAS,QAAQ,CAAC,OAAO,CAAC,KAAK;YAChD,MAAMK,YACJD,AAAa,aAAbA,WAAwB,IAAIA,AAAa,aAAbA,WAAwB,IAAI;YAE1D,MAAMP,aAAaE,gBAAgB;gBACjC,MAAMM;gBACN,MAAML,SAAS,QAAQ;gBACvBE;gBACA,GAAIF,SAAS,QAAQ,GACjB;oBACE,QAAQM,mBAAmBN,SAAS,QAAQ;oBAC5C,UAAUM,mBAAmBN,SAAS,QAAQ,IAAI;gBACpD,IACA,CAAC,CAAC;YACR;YACAP,WAAW,uCAAuC;gBAChD,MAAMY;gBACN,MAAML,SAAS,QAAQ;gBACvB,MAAME;YACR;YACA,OAAOL;QACT,EAAE,OAAOU,OAAO;YACdhB,UAAU,oCAAoCgB;YAC9C,MAAM,IAAIN,MACR,CAAC,yBAAyB,EAAEZ,WAAW,+GAA+G,CAAC;QAE3J;IAEJ;AACF"}
@@ -1,10 +1,24 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) Object.defineProperty(obj, key, {
3
+ value: value,
4
+ enumerable: true,
5
+ configurable: true,
6
+ writable: true
7
+ });
8
+ else obj[key] = value;
9
+ return obj;
10
+ }
1
11
  const DEFAULT_AI_CALL_TIMEOUT_MS = 180000;
2
12
  const AI_CALL_HARD_TIMEOUT_CODE = 'AI_CALL_HARD_TIMEOUT';
3
- function resolveEffectiveTimeoutMs(modelConfig) {
4
- const { timeout } = modelConfig;
5
- if ('number' != typeof timeout) return DEFAULT_AI_CALL_TIMEOUT_MS;
6
- if (timeout <= 0) return null;
7
- return timeout;
13
+ class AIRequestTimeoutError extends Error {
14
+ constructor(timeoutMs){
15
+ super(`AI call hard timeout after ${timeoutMs}ms (full request time exceeded)`), _define_property(this, "timeoutMs", void 0), _define_property(this, "code", void 0), this.timeoutMs = timeoutMs, this.code = AI_CALL_HARD_TIMEOUT_CODE;
16
+ this.name = 'AIRequestTimeoutError';
17
+ }
18
+ }
19
+ function resolveEffectiveTimeoutMs(timeout) {
20
+ if (void 0 === timeout) return DEFAULT_AI_CALL_TIMEOUT_MS;
21
+ return 0 === timeout ? null : timeout;
8
22
  }
9
23
  function isHardTimeoutError(err) {
10
24
  if (!err || 'object' != typeof err) return false;
@@ -14,15 +28,6 @@ function isHardTimeoutError(err) {
14
28
  if (cause && 'object' == typeof cause && cause.code === AI_CALL_HARD_TIMEOUT_CODE) return true;
15
29
  return false;
16
30
  }
17
- function restoreHardTimeoutError(error, signal) {
18
- if (!signal.aborted || !isHardTimeoutError(signal.reason)) return error;
19
- const timeoutReason = signal.reason;
20
- const restored = new Error(timeoutReason.message, {
21
- cause: error
22
- });
23
- restored.code = AI_CALL_HARD_TIMEOUT_CODE;
24
- return restored;
25
- }
26
31
  function buildRequestAbortSignal(timeoutMs, userSignal) {
27
32
  const controller = new AbortController();
28
33
  if (userSignal?.aborted) {
@@ -35,9 +40,7 @@ function buildRequestAbortSignal(timeoutMs, userSignal) {
35
40
  let timer;
36
41
  if (null !== timeoutMs) {
37
42
  timer = setTimeout(()=>{
38
- const err = new Error(`AI call hard timeout after ${timeoutMs}ms (full request time exceeded)`);
39
- err.code = AI_CALL_HARD_TIMEOUT_CODE;
40
- controller.abort(err);
43
+ controller.abort(new AIRequestTimeoutError(timeoutMs));
41
44
  }, timeoutMs);
42
45
  if ('function' == typeof timer.unref) timer.unref();
43
46
  }
@@ -53,6 +56,44 @@ function buildRequestAbortSignal(timeoutMs, userSignal) {
53
56
  }
54
57
  };
55
58
  }
56
- export { AI_CALL_HARD_TIMEOUT_CODE, DEFAULT_AI_CALL_TIMEOUT_MS, buildRequestAbortSignal, isHardTimeoutError, resolveEffectiveTimeoutMs, restoreHardTimeoutError };
59
+ async function runWithAbortSignal(signal, operation) {
60
+ signal.throwIfAborted();
61
+ let onAbort;
62
+ const aborted = new Promise((_resolve, reject)=>{
63
+ onAbort = ()=>reject(signal.reason);
64
+ signal.addEventListener('abort', onAbort, {
65
+ once: true
66
+ });
67
+ });
68
+ try {
69
+ return await Promise.race([
70
+ operation(),
71
+ aborted
72
+ ]);
73
+ } finally{
74
+ if (onAbort) signal.removeEventListener('abort', onAbort);
75
+ }
76
+ }
77
+ function waitForRetry(ms, signal) {
78
+ signal?.throwIfAborted();
79
+ return new Promise((resolve, reject)=>{
80
+ const cleanup = ()=>{
81
+ clearTimeout(timer);
82
+ signal?.removeEventListener('abort', onAbort);
83
+ };
84
+ const onAbort = ()=>{
85
+ cleanup();
86
+ reject(signal?.reason);
87
+ };
88
+ const timer = setTimeout(()=>{
89
+ cleanup();
90
+ resolve();
91
+ }, ms);
92
+ signal?.addEventListener('abort', onAbort, {
93
+ once: true
94
+ });
95
+ });
96
+ }
97
+ export { AIRequestTimeoutError, AI_CALL_HARD_TIMEOUT_CODE, DEFAULT_AI_CALL_TIMEOUT_MS, buildRequestAbortSignal, isHardTimeoutError, resolveEffectiveTimeoutMs, runWithAbortSignal, waitForRetry };
57
98
 
58
99
  //# sourceMappingURL=request-timeout.mjs.map
@@ -1 +1 @@
1
- {"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/**\n * The OpenAI SDK converts an abort from a caller-provided signal into an\n * APIUserAbortError and discards the signal's reason. Restore our timeout\n * reason before surfacing the error to callers (and consequently reports).\n */\nexport function restoreHardTimeoutError(\n error: Error,\n signal: AbortSignal,\n): Error {\n if (!signal.aborted || !isHardTimeoutError(signal.reason)) {\n return error;\n }\n\n const timeoutReason = signal.reason as Error & { code?: string };\n const restored = new Error(timeoutReason.message, {\n cause: error,\n }) as Error & {\n code?: string;\n };\n restored.code = AI_CALL_HARD_TIMEOUT_CODE;\n return restored;\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","restoreHardTimeoutError","error","signal","timeoutReason","restored","Error","buildRequestAbortSignal","timeoutMs","userSignal","controller","AbortController","timer","setTimeout","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,KAAY,EACZC,MAAmB;IAEnB,IAAI,CAACA,OAAO,OAAO,IAAI,CAACN,mBAAmBM,OAAO,MAAM,GACtD,OAAOD;IAGT,MAAME,gBAAgBD,OAAO,MAAM;IACnC,MAAME,WAAW,IAAIC,MAAMF,cAAc,OAAO,EAAE;QAChD,OAAOF;IACT;IAGAG,SAAS,IAAI,GAAGZ;IAChB,OAAOY;AACT;AAOO,SAASE,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,MAAMf,MAAM,IAAIQ,MACd,CAAC,2BAA2B,EAAEE,UAAU,+BAA+B,CAAC;YAE1EV,IAAI,IAAI,GAAGL;YACXiB,WAAW,KAAK,CAACZ;QACnB,GAAGU;QACH,IAAI,AAAmD,cAAnD,OAAQI,MAAiC,KAAK,EAC/CA,MAAgC,KAAK;IAE1C;IAEA,MAAME,cAAcL,aAChB,IAAMC,WAAW,KAAK,CAACD,WAAW,MAAM,IACxCM;IACJ,IAAIN,cAAcK,aAChBL,WAAW,gBAAgB,CAAC,SAASK,aAAa;QAAE,MAAM;IAAK;IAGjE,OAAO;QACL,QAAQJ,WAAW,MAAM;QACzB,SAAS;YACP,IAAIE,OAAOI,aAAaJ;YACxB,IAAIH,cAAcK,aAChBL,WAAW,mBAAmB,CAAC,SAASK;QAE5C;IACF;AACF"}
1
+ {"version":3,"file":"ai-model/service-caller/request-timeout.mjs","sources":["../../../../src/ai-model/service-caller/request-timeout.ts"],"sourcesContent":["/**\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. SDK and network timeouts still apply, and a caller-provided\n * `abortSignal` can still cancel the request. Each retry gets a fresh timeout;\n * retry waiting is not included in that timeout.\n */\nexport const DEFAULT_AI_CALL_TIMEOUT_MS = 180_000;\n\n/** Identifying code for Midscene single-request timeouts. */\nexport const AI_CALL_HARD_TIMEOUT_CODE = 'AI_CALL_HARD_TIMEOUT';\n\nexport class AIRequestTimeoutError extends Error {\n readonly code = AI_CALL_HARD_TIMEOUT_CODE;\n\n constructor(readonly timeoutMs: number) {\n super(\n `AI call hard timeout after ${timeoutMs}ms (full request time exceeded)`,\n );\n this.name = 'AIRequestTimeoutError';\n }\n}\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(timeout?: number): number | null {\n if (timeout === undefined) {\n return DEFAULT_AI_CALL_TIMEOUT_MS;\n }\n return timeout === 0 ? null : 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 controller.abort(new AIRequestTimeoutError(timeoutMs));\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\n/** Stops awaiting promptly while still forwarding cancellation to the actual request. */\nexport async function runWithAbortSignal<T>(\n signal: AbortSignal,\n operation: () => Promise<T>,\n): Promise<T> {\n signal.throwIfAborted();\n let onAbort: (() => void) | undefined;\n const aborted = new Promise<never>((_resolve, reject) => {\n onAbort = () => reject(signal.reason);\n signal.addEventListener('abort', onAbort, { once: true });\n });\n try {\n return await Promise.race([operation(), aborted]);\n } finally {\n if (onAbort) {\n signal.removeEventListener('abort', onAbort);\n }\n }\n}\n\n/** Both completion paths remove the listener and cancel the unused timer. */\nexport function waitForRetry(ms: number, signal?: AbortSignal): Promise<void> {\n signal?.throwIfAborted();\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n };\n const onAbort = () => {\n cleanup();\n reject(signal?.reason);\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"names":["DEFAULT_AI_CALL_TIMEOUT_MS","AI_CALL_HARD_TIMEOUT_CODE","AIRequestTimeoutError","Error","timeoutMs","resolveEffectiveTimeoutMs","timeout","undefined","isHardTimeoutError","err","code","cause","buildRequestAbortSignal","userSignal","controller","AbortController","timer","setTimeout","onUserAbort","clearTimeout","runWithAbortSignal","signal","operation","onAbort","aborted","Promise","_resolve","reject","waitForRetry","ms","resolve","cleanup"],"mappings":"AAaC;;;;;;;;;;AACM,MAAMA,6BAA6B;AAGnC,MAAMC,4BAA4B;AAElC,MAAMC,8BAA8BC;IAGzC,YAAqBC,SAAiB,CAAE;QACtC,KAAK,CACH,CAAC,2BAA2B,EAAEA,UAAU,+BAA+B,CAAC,gDAJ5E,uBAAS,QAAT,cAEqBA,SAAS,GAATA,WAAAA,IAAAA,CAFZ,IAAI,GAAGH;QAMd,IAAI,CAAC,IAAI,GAAG;IACd;AACF;AAMO,SAASI,0BAA0BC,OAAgB;IACxD,IAAIA,AAAYC,WAAZD,SACF,OAAON;IAET,OAAOM,AAAY,MAAZA,UAAgB,OAAOA;AAChC;AAOO,SAASE,mBAAmBC,GAAY;IAC7C,IAAI,CAACA,OAAO,AAAe,YAAf,OAAOA,KAAkB,OAAO;IAC5C,MAAMC,OAAQD,IAA2B,IAAI;IAC7C,IAAIC,SAAST,2BAA2B,OAAO;IAC/C,MAAMU,QAASF,IAA4B,KAAK;IAChD,IACEE,SACA,AAAiB,YAAjB,OAAOA,SACNA,MAA6B,IAAI,KAAKV,2BAEvC,OAAO;IAET,OAAO;AACT;AAOO,SAASW,wBACdR,SAAwB,EACxBS,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,IAAIZ,AAAc,SAAdA,WAAoB;QACtBY,QAAQC,WAAW;YACjBH,WAAW,KAAK,CAAC,IAAIZ,sBAAsBE;QAC7C,GAAGA;QACH,IAAI,AAAmD,cAAnD,OAAQY,MAAiC,KAAK,EAC/CA,MAAgC,KAAK;IAE1C;IAEA,MAAME,cAAcL,aAChB,IAAMC,WAAW,KAAK,CAACD,WAAW,MAAM,IACxCN;IACJ,IAAIM,cAAcK,aAChBL,WAAW,gBAAgB,CAAC,SAASK,aAAa;QAAE,MAAM;IAAK;IAGjE,OAAO;QACL,QAAQJ,WAAW,MAAM;QACzB,SAAS;YACP,IAAIE,OAAOG,aAAaH;YACxB,IAAIH,cAAcK,aAChBL,WAAW,mBAAmB,CAAC,SAASK;QAE5C;IACF;AACF;AAGO,eAAeE,mBACpBC,MAAmB,EACnBC,SAA2B;IAE3BD,OAAO,cAAc;IACrB,IAAIE;IACJ,MAAMC,UAAU,IAAIC,QAAe,CAACC,UAAUC;QAC5CJ,UAAU,IAAMI,OAAON,OAAO,MAAM;QACpCA,OAAO,gBAAgB,CAAC,SAASE,SAAS;YAAE,MAAM;QAAK;IACzD;IACA,IAAI;QACF,OAAO,MAAME,QAAQ,IAAI,CAAC;YAACH;YAAaE;SAAQ;IAClD,SAAU;QACR,IAAID,SACFF,OAAO,mBAAmB,CAAC,SAASE;IAExC;AACF;AAGO,SAASK,aAAaC,EAAU,EAAER,MAAoB;IAC3DA,QAAQ;IACR,OAAO,IAAII,QAAQ,CAACK,SAASH;QAC3B,MAAMI,UAAU;YACdZ,aAAaH;YACbK,QAAQ,oBAAoB,SAASE;QACvC;QACA,MAAMA,UAAU;YACdQ;YACAJ,OAAON,QAAQ;QACjB;QACA,MAAML,QAAQC,WAAW;YACvBc;YACAD;QACF,GAAGD;QACHR,QAAQ,iBAAiB,SAASE,SAAS;YAAE,MAAM;QAAK;IAC1D;AACF"}
@@ -1,3 +1,4 @@
1
+ import { waitForRetry } from "./request-timeout.mjs";
1
2
  function withSemanticRetryFeedback(messages, previousParseError) {
2
3
  if (!previousParseError) return messages;
3
4
  const errorMessage = previousParseError instanceof Error ? previousParseError.message : String(previousParseError);
@@ -13,14 +14,18 @@ async function callAiAndParseWithRetry({ callAi, parseResponse, toParseError, pa
13
14
  const normalizedRetryTimes = Number.isFinite(parseRetryTimes) ? Math.max(0, Math.floor(parseRetryTimes)) : 0;
14
15
  const normalizedRetryInterval = Number.isFinite(parseRetryInterval) ? Math.max(0, parseRetryInterval) : 0;
15
16
  const callAndParseOnce = async (remainingRetries, retryAttempt, previousParseError)=>{
17
+ abortSignal?.throwIfAborted();
16
18
  const response = await callAi(retryAttempt, previousParseError);
17
19
  try {
18
- return await parseResponse(response);
20
+ const parsed = await parseResponse(response);
21
+ abortSignal?.throwIfAborted();
22
+ return parsed;
19
23
  } catch (error) {
20
- if (remainingRetries > 0 && !abortSignal?.aborted) {
24
+ abortSignal?.throwIfAborted();
25
+ if (remainingRetries > 0) {
21
26
  onParseRetry?.(error, response);
22
- if (normalizedRetryInterval > 0) await new Promise((resolve)=>setTimeout(resolve, normalizedRetryInterval));
23
- if (abortSignal?.aborted) throw toParseError(error, response);
27
+ if (normalizedRetryInterval > 0) await waitForRetry(normalizedRetryInterval, abortSignal);
28
+ abortSignal?.throwIfAborted();
24
29
  return callAndParseOnce(remainingRetries - 1, retryAttempt + 1, error);
25
30
  }
26
31
  throw toParseError(error, response);
@@ -1 +1 @@
1
- {"version":3,"file":"ai-model/service-caller/semantic-retry.mjs","sources":["../../../../src/ai-model/service-caller/semantic-retry.ts"],"sourcesContent":["import type { ChatCompletionMessageParam } from 'openai/resources/index';\n\nexport function withSemanticRetryFeedback(\n messages: ChatCompletionMessageParam[],\n previousParseError?: unknown,\n): ChatCompletionMessageParam[] {\n if (!previousParseError) return messages;\n\n const errorMessage =\n previousParseError instanceof Error\n ? previousParseError.message\n : String(previousParseError);\n\n return [\n ...messages,\n {\n role: 'user',\n content: `The previous response was invalid:\\n${errorMessage}\\n\\nPlease avoid the validation error above in this response.`,\n },\n ];\n}\n\nexport type CallAiAndParseWithRetryOptions<Response, Parsed> = {\n /** Sends a single model request. Request errors are always propagated as-is. */\n /**\n * `retryAttempt` is local to this retry loop: 0 for the initial request,\n * then incremented after each parsing failure.\n */\n callAi: (\n retryAttempt: number,\n previousParseError?: unknown,\n ) => Promise<Response>;\n /** Parses a successful model response. Only failures from this callback retry. */\n parseResponse: (response: Response) => Parsed | Promise<Parsed>;\n /** Converts the final parsing failure into the caller's domain error. */\n toParseError: (error: unknown, response: Response) => Error;\n /** Number of additional model requests after a parsing failure. Defaults to 0. */\n parseRetryTimes?: number;\n /** Delay in milliseconds before retrying a parsing failure. Defaults to 0. */\n parseRetryInterval?: number;\n abortSignal?: AbortSignal;\n /** Runs immediately before a parsing failure triggers another model request. */\n onParseRetry?: (error: unknown, response: Response) => void;\n};\n\n/**\n * Calls AI and parses its response, retrying only when the response cannot be\n * structurally parsed.\n *\n * Errors thrown by callAi() intentionally bypass this retry mechanism and are\n * propagated as-is. Only a successful callAi() response that fails the\n * parseResponse() callback triggers another model request.\n */\nexport async function callAiAndParseWithRetry<Response, Parsed>({\n callAi,\n parseResponse,\n toParseError,\n parseRetryTimes = 0,\n parseRetryInterval = 0,\n abortSignal,\n onParseRetry,\n}: CallAiAndParseWithRetryOptions<Response, Parsed>): Promise<Parsed> {\n const normalizedRetryTimes = Number.isFinite(parseRetryTimes)\n ? Math.max(0, Math.floor(parseRetryTimes))\n : 0;\n const normalizedRetryInterval = Number.isFinite(parseRetryInterval)\n ? Math.max(0, parseRetryInterval)\n : 0;\n\n const callAndParseOnce = async (\n remainingRetries: number,\n retryAttempt: number,\n previousParseError?: unknown,\n ): Promise<Parsed> => {\n const response = await callAi(retryAttempt, previousParseError);\n\n try {\n return await parseResponse(response);\n } catch (error) {\n if (remainingRetries > 0 && !abortSignal?.aborted) {\n onParseRetry?.(error, response);\n if (normalizedRetryInterval > 0) {\n await new Promise((resolve) =>\n setTimeout(resolve, normalizedRetryInterval),\n );\n }\n if (abortSignal?.aborted) {\n throw toParseError(error, response);\n }\n return callAndParseOnce(remainingRetries - 1, retryAttempt + 1, error);\n }\n throw toParseError(error, response);\n }\n };\n\n return callAndParseOnce(normalizedRetryTimes, 0);\n}\n"],"names":["withSemanticRetryFeedback","messages","previousParseError","errorMessage","Error","String","callAiAndParseWithRetry","callAi","parseResponse","toParseError","parseRetryTimes","parseRetryInterval","abortSignal","onParseRetry","normalizedRetryTimes","Number","Math","normalizedRetryInterval","callAndParseOnce","remainingRetries","retryAttempt","response","error","Promise","resolve","setTimeout"],"mappings":"AAEO,SAASA,0BACdC,QAAsC,EACtCC,kBAA4B;IAE5B,IAAI,CAACA,oBAAoB,OAAOD;IAEhC,MAAME,eACJD,8BAA8BE,QAC1BF,mBAAmB,OAAO,GAC1BG,OAAOH;IAEb,OAAO;WACFD;QACH;YACE,MAAM;YACN,SAAS,CAAC,oCAAoC,EAAEE,aAAa,6DAA6D,CAAC;QAC7H;KACD;AACH;AAiCO,eAAeG,wBAA0C,EAC9DC,MAAM,EACNC,aAAa,EACbC,YAAY,EACZC,kBAAkB,CAAC,EACnBC,qBAAqB,CAAC,EACtBC,WAAW,EACXC,YAAY,EACqC;IACjD,MAAMC,uBAAuBC,OAAO,QAAQ,CAACL,mBACzCM,KAAK,GAAG,CAAC,GAAGA,KAAK,KAAK,CAACN,oBACvB;IACJ,MAAMO,0BAA0BF,OAAO,QAAQ,CAACJ,sBAC5CK,KAAK,GAAG,CAAC,GAAGL,sBACZ;IAEJ,MAAMO,mBAAmB,OACvBC,kBACAC,cACAlB;QAEA,MAAMmB,WAAW,MAAMd,OAAOa,cAAclB;QAE5C,IAAI;YACF,OAAO,MAAMM,cAAca;QAC7B,EAAE,OAAOC,OAAO;YACd,IAAIH,mBAAmB,KAAK,CAACP,aAAa,SAAS;gBACjDC,eAAeS,OAAOD;gBACtB,IAAIJ,0BAA0B,GAC5B,MAAM,IAAIM,QAAQ,CAACC,UACjBC,WAAWD,SAASP;gBAGxB,IAAIL,aAAa,SACf,MAAMH,aAAaa,OAAOD;gBAE5B,OAAOH,iBAAiBC,mBAAmB,GAAGC,eAAe,GAAGE;YAClE;YACA,MAAMb,aAAaa,OAAOD;QAC5B;IACF;IAEA,OAAOH,iBAAiBJ,sBAAsB;AAChD"}
1
+ {"version":3,"file":"ai-model/service-caller/semantic-retry.mjs","sources":["../../../../src/ai-model/service-caller/semantic-retry.ts"],"sourcesContent":["import type { ChatCompletionMessageParam } from 'openai/resources/index';\nimport { waitForRetry } from './request-timeout';\n\nexport function withSemanticRetryFeedback(\n messages: ChatCompletionMessageParam[],\n previousParseError?: unknown,\n): ChatCompletionMessageParam[] {\n if (!previousParseError) return messages;\n\n const errorMessage =\n previousParseError instanceof Error\n ? previousParseError.message\n : String(previousParseError);\n\n return [\n ...messages,\n {\n role: 'user',\n content: `The previous response was invalid:\\n${errorMessage}\\n\\nPlease avoid the validation error above in this response.`,\n },\n ];\n}\n\nexport type CallAiAndParseWithRetryOptions<Response, Parsed> = {\n /** Sends a single model request. Request errors are always propagated as-is. */\n /**\n * `retryAttempt` is local to this retry loop: 0 for the initial request,\n * then incremented after each parsing failure.\n */\n callAi: (\n retryAttempt: number,\n previousParseError?: unknown,\n ) => Promise<Response>;\n /** Parses a successful model response. Only failures from this callback retry. */\n parseResponse: (response: Response) => Parsed | Promise<Parsed>;\n /** Converts the final parsing failure into the caller's domain error. */\n toParseError: (error: unknown, response: Response) => Error;\n /** Number of additional model requests after a parsing failure. Defaults to 0. */\n parseRetryTimes?: number;\n /** Delay in milliseconds before retrying a parsing failure. Defaults to 0. */\n parseRetryInterval?: number;\n abortSignal?: AbortSignal;\n /** Runs immediately before a parsing failure triggers another model request. */\n onParseRetry?: (error: unknown, response: Response) => void;\n};\n\n/**\n * Calls AI and parses its response, retrying only when the response cannot be\n * structurally parsed.\n *\n * Errors thrown by callAi() intentionally bypass this retry mechanism and are\n * propagated as-is. Only a successful callAi() response that fails the\n * parseResponse() callback triggers another model request.\n */\nexport async function callAiAndParseWithRetry<Response, Parsed>({\n callAi,\n parseResponse,\n toParseError,\n parseRetryTimes = 0,\n parseRetryInterval = 0,\n abortSignal,\n onParseRetry,\n}: CallAiAndParseWithRetryOptions<Response, Parsed>): Promise<Parsed> {\n const normalizedRetryTimes = Number.isFinite(parseRetryTimes)\n ? Math.max(0, Math.floor(parseRetryTimes))\n : 0;\n const normalizedRetryInterval = Number.isFinite(parseRetryInterval)\n ? Math.max(0, parseRetryInterval)\n : 0;\n\n const callAndParseOnce = async (\n remainingRetries: number,\n retryAttempt: number,\n previousParseError?: unknown,\n ): Promise<Parsed> => {\n abortSignal?.throwIfAborted();\n const response = await callAi(retryAttempt, previousParseError);\n\n try {\n const parsed = await parseResponse(response);\n abortSignal?.throwIfAborted();\n return parsed;\n } catch (error) {\n abortSignal?.throwIfAborted();\n if (remainingRetries > 0) {\n onParseRetry?.(error, response);\n if (normalizedRetryInterval > 0) {\n await waitForRetry(normalizedRetryInterval, abortSignal);\n }\n abortSignal?.throwIfAborted();\n return callAndParseOnce(remainingRetries - 1, retryAttempt + 1, error);\n }\n throw toParseError(error, response);\n }\n };\n\n return callAndParseOnce(normalizedRetryTimes, 0);\n}\n"],"names":["withSemanticRetryFeedback","messages","previousParseError","errorMessage","Error","String","callAiAndParseWithRetry","callAi","parseResponse","toParseError","parseRetryTimes","parseRetryInterval","abortSignal","onParseRetry","normalizedRetryTimes","Number","Math","normalizedRetryInterval","callAndParseOnce","remainingRetries","retryAttempt","response","parsed","error","waitForRetry"],"mappings":";AAGO,SAASA,0BACdC,QAAsC,EACtCC,kBAA4B;IAE5B,IAAI,CAACA,oBAAoB,OAAOD;IAEhC,MAAME,eACJD,8BAA8BE,QAC1BF,mBAAmB,OAAO,GAC1BG,OAAOH;IAEb,OAAO;WACFD;QACH;YACE,MAAM;YACN,SAAS,CAAC,oCAAoC,EAAEE,aAAa,6DAA6D,CAAC;QAC7H;KACD;AACH;AAiCO,eAAeG,wBAA0C,EAC9DC,MAAM,EACNC,aAAa,EACbC,YAAY,EACZC,kBAAkB,CAAC,EACnBC,qBAAqB,CAAC,EACtBC,WAAW,EACXC,YAAY,EACqC;IACjD,MAAMC,uBAAuBC,OAAO,QAAQ,CAACL,mBACzCM,KAAK,GAAG,CAAC,GAAGA,KAAK,KAAK,CAACN,oBACvB;IACJ,MAAMO,0BAA0BF,OAAO,QAAQ,CAACJ,sBAC5CK,KAAK,GAAG,CAAC,GAAGL,sBACZ;IAEJ,MAAMO,mBAAmB,OACvBC,kBACAC,cACAlB;QAEAU,aAAa;QACb,MAAMS,WAAW,MAAMd,OAAOa,cAAclB;QAE5C,IAAI;YACF,MAAMoB,SAAS,MAAMd,cAAca;YACnCT,aAAa;YACb,OAAOU;QACT,EAAE,OAAOC,OAAO;YACdX,aAAa;YACb,IAAIO,mBAAmB,GAAG;gBACxBN,eAAeU,OAAOF;gBACtB,IAAIJ,0BAA0B,GAC5B,MAAMO,aAAaP,yBAAyBL;gBAE9CA,aAAa;gBACb,OAAOM,iBAAiBC,mBAAmB,GAAGC,eAAe,GAAGG;YAClE;YACA,MAAMd,aAAac,OAAOF;QAC5B;IACF;IAEA,OAAOH,iBAAiBJ,sBAAsB;AAChD"}
@@ -31,12 +31,6 @@ function stringifyForDebug(value) {
31
31
  return String(value);
32
32
  }
33
33
  }
34
- function getLatestSuccessfulResponseRequestId(context) {
35
- return context.responseRequestIds?.reduce((latestRequestId, response)=>response.ok ? response.requestId : latestRequestId, void 0);
36
- }
37
- function getLatestResponseAttempt(context) {
38
- return context.httpResponses?.at(-1)?.attempt ?? 1;
39
- }
40
34
  function getErrorMessage(error) {
41
35
  return error instanceof Error ? error.message : String(error);
42
36
  }
@@ -59,7 +53,7 @@ function appendAIRequestFailureSummary(error, attemptErrors, maxAttempts) {
59
53
  error.message = `${error.message}\nPrevious AI call attempt errors:\n${details}`;
60
54
  return error;
61
55
  }
62
- const buildUsageInfo = ({ usageData, requestId, timeCost, modelName, modelDescription, responseModelName, slot, internalCallId })=>{
56
+ const buildUsageInfo = ({ usageData, requestId, timeCost, totalTimeCost, retryCount, modelName, modelDescription, responseModelName, slot, internalCallId })=>{
63
57
  if (!usageData) return;
64
58
  const cachedInputTokens = usageData?.prompt_tokens_details?.cached_tokens;
65
59
  return {
@@ -69,6 +63,8 @@ const buildUsageInfo = ({ usageData, requestId, timeCost, modelName, modelDescri
69
63
  total_tokens: usageData.total_tokens ?? 0,
70
64
  cached_input: cachedInputTokens ?? 0,
71
65
  time_cost: timeCost ?? 0,
66
+ total_time_cost: totalTimeCost,
67
+ retry_count: retryCount,
72
68
  model_name: modelName,
73
69
  model_description: modelDescription,
74
70
  response_model_name: responseModelName,
@@ -78,6 +74,27 @@ const buildUsageInfo = ({ usageData, requestId, timeCost, modelName, modelDescri
78
74
  [INTERNAL_CALL_ID_FIELD]: internalCallId
79
75
  };
80
76
  };
81
- export { AIResponseParseError, INTERNAL_CALL_ID_FIELD, appendAIRequestFailureSummary, buildUsageInfo, getLatestResponseAttempt, getLatestSuccessfulResponseRequestId, nextInternalCallId, normalizeRetryCount, stringifyForDebug, toError };
77
+ const applyImageDetail = ({ imageDetail, messages })=>{
78
+ if (!imageDetail) return messages;
79
+ return messages.map((msg)=>{
80
+ if (!Array.isArray(msg.content)) return msg;
81
+ const content = msg.content.map((part)=>{
82
+ if (part && 'image_url' === part.type && part.image_url?.url) return {
83
+ ...part,
84
+ image_url: {
85
+ ...part.image_url,
86
+ detail: imageDetail
87
+ }
88
+ };
89
+ return part;
90
+ });
91
+ return {
92
+ ...msg,
93
+ content
94
+ };
95
+ });
96
+ };
97
+ const hasUsableText = (value)=>'string' == typeof value && value.trim().length > 0;
98
+ export { AIResponseParseError, INTERNAL_CALL_ID_FIELD, appendAIRequestFailureSummary, applyImageDetail, buildUsageInfo, hasUsableText, nextInternalCallId, normalizeRetryCount, stringifyForDebug, toError };
82
99
 
83
100
  //# sourceMappingURL=utils.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"ai-model/service-caller/utils.mjs","sources":["../../../../src/ai-model/service-caller/utils.ts"],"sourcesContent":["import type { AIUsageInfo } from '@/types';\nimport type OpenAI from 'openai';\nimport type { ModelRuntime } from '../models';\nimport type { OpenAIErrorResponseContext } from './openai-error';\n\n// Error class that preserves usage and rawResponse when AI call parsing fails\nexport class AIResponseParseError extends Error {\n usage?: AIUsageInfo;\n /**\n * Adapter-extracted content used by Midscene for parsing. This is not the\n * full provider response or choices[0].message.\n */\n rawResponse: string;\n rawChoiceMessage?: unknown;\n reasoningContent?: string;\n\n constructor(\n message: string,\n rawResponse: string,\n usage?: AIUsageInfo,\n rawChoiceMessage?: unknown,\n reasoningContent?: string,\n ) {\n super(message);\n this.name = 'AIResponseParseError';\n this.rawResponse = rawResponse;\n this.usage = usage;\n this.rawChoiceMessage = rawChoiceMessage;\n this.reasoningContent = reasoningContent;\n }\n}\n\n/**\n * Internal field name stamped onto every AIUsageInfo shaped by callAI().\n * Used for cross-path dedup when the provider does not return a request_id.\n */\nexport const INTERNAL_CALL_ID_FIELD = '_midscene_call_id';\n\nlet internalCallIdCounter = 0;\nexport function nextInternalCallId(): string {\n internalCallIdCounter += 1;\n return `call_${internalCallIdCounter}`;\n}\n\nexport function stringifyForDebug(value: unknown): string {\n try {\n return JSON.stringify(value);\n } catch (_error) {\n return String(value);\n }\n}\n\nexport function getLatestSuccessfulResponseRequestId(\n context: OpenAIErrorResponseContext,\n): string | undefined {\n return context.responseRequestIds?.reduce<string | undefined>(\n (latestRequestId, response) =>\n response.ok ? response.requestId : latestRequestId,\n undefined,\n );\n}\n\nexport function getLatestResponseAttempt(context: OpenAIErrorResponseContext) {\n return context.httpResponses?.at(-1)?.attempt ?? 1;\n}\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nexport function normalizeRetryCount(retryCount: unknown): number {\n if (typeof retryCount !== 'number' || !Number.isFinite(retryCount)) {\n return 1;\n }\n\n return Math.max(0, Math.floor(retryCount));\n}\n\nexport function appendAIRequestFailureSummary<T extends Error>(\n error: T,\n attemptErrors: Array<{ attempt: number; error: unknown }>,\n maxAttempts: number,\n): T {\n const failedAttempts = attemptErrors.length;\n const retries = Math.max(0, failedAttempts - 1);\n const retryLabel = retries === 1 ? 'retry' : 'retries';\n const originalMessage = error.message;\n const previousAttemptErrors = attemptErrors.slice(0, -1);\n\n error.message = `AI model request failed after ${retries} ${retryLabel} (${failedAttempts}/${maxAttempts} attempts). Last error: ${originalMessage}`;\n\n if (previousAttemptErrors.length === 0) {\n return error;\n }\n\n const details = previousAttemptErrors\n .map(\n ({ attempt, error }) => `Attempt ${attempt}: ${getErrorMessage(error)}`,\n )\n .join('\\n');\n\n error.message = `${error.message}\\nPrevious AI call attempt errors:\\n${details}`;\n return error;\n}\n\nexport const buildUsageInfo = ({\n usageData,\n requestId,\n timeCost,\n modelName,\n modelDescription,\n responseModelName,\n slot,\n internalCallId,\n}: {\n usageData?: OpenAI.CompletionUsage;\n requestId?: string | null;\n timeCost?: number;\n modelName: string;\n modelDescription: string;\n responseModelName?: string;\n slot: ModelRuntime['config']['slot'];\n internalCallId: string;\n}): AIUsageInfo | undefined => {\n if (!usageData) return undefined;\n\n const cachedInputTokens = (\n usageData as { prompt_tokens_details?: { cached_tokens?: number } }\n )?.prompt_tokens_details?.cached_tokens;\n\n return {\n ...usageData,\n prompt_tokens: usageData.prompt_tokens ?? 0,\n completion_tokens: usageData.completion_tokens ?? 0,\n total_tokens: usageData.total_tokens ?? 0,\n cached_input: cachedInputTokens ?? 0,\n time_cost: timeCost ?? 0,\n model_name: modelName,\n model_description: modelDescription,\n response_model_name: responseModelName,\n slot,\n // Left undefined at the raw call layer. The agent's onUsage callback\n // fills it from modelConfig.slot for metrics collection, and task\n // layers use withUsageIntent() to stamp a more specific semantic\n // intent (e.g. 'planning', 'insight') when attaching usage to tasks.\n intent: undefined,\n request_id: requestId ?? undefined,\n // Internal stable ID for cross-path dedup when request_id is absent.\n [INTERNAL_CALL_ID_FIELD]: internalCallId,\n } satisfies AIUsageInfo;\n};\n"],"names":["AIResponseParseError","Error","message","rawResponse","usage","rawChoiceMessage","reasoningContent","INTERNAL_CALL_ID_FIELD","internalCallIdCounter","nextInternalCallId","stringifyForDebug","value","JSON","_error","String","getLatestSuccessfulResponseRequestId","context","latestRequestId","response","undefined","getLatestResponseAttempt","getErrorMessage","error","toError","normalizeRetryCount","retryCount","Number","Math","appendAIRequestFailureSummary","attemptErrors","maxAttempts","failedAttempts","retries","retryLabel","originalMessage","previousAttemptErrors","details","attempt","buildUsageInfo","usageData","requestId","timeCost","modelName","modelDescription","responseModelName","slot","internalCallId","cachedInputTokens"],"mappings":"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"}
1
+ {"version":3,"file":"ai-model/service-caller/utils.mjs","sources":["../../../../src/ai-model/service-caller/utils.ts"],"sourcesContent":["import type { AIUsageInfo } from '@/types';\nimport type OpenAI from 'openai';\nimport type { ChatCompletionMessageParam } from 'openai/resources/index';\nimport type { ImageDetail } from '../model-adapter/types';\nimport type { ModelRuntime } from '../models';\n\n// Error class that preserves usage and rawResponse when AI call parsing fails\nexport class AIResponseParseError extends Error {\n usage?: AIUsageInfo;\n /**\n * Adapter-extracted content used by Midscene for parsing. This is not the\n * full provider response or choices[0].message.\n */\n rawResponse: string;\n rawChoiceMessage?: unknown;\n reasoningContent?: string;\n\n constructor(\n message: string,\n rawResponse: string,\n usage?: AIUsageInfo,\n rawChoiceMessage?: unknown,\n reasoningContent?: string,\n ) {\n super(message);\n this.name = 'AIResponseParseError';\n this.rawResponse = rawResponse;\n this.usage = usage;\n this.rawChoiceMessage = rawChoiceMessage;\n this.reasoningContent = reasoningContent;\n }\n}\n\n/**\n * Internal field name stamped onto every AIUsageInfo shaped by callAI().\n * Used for cross-path dedup when the provider does not return a request_id.\n */\nexport const INTERNAL_CALL_ID_FIELD = '_midscene_call_id';\n\nlet internalCallIdCounter = 0;\nexport function nextInternalCallId(): string {\n internalCallIdCounter += 1;\n return `call_${internalCallIdCounter}`;\n}\n\nexport function stringifyForDebug(value: unknown): string {\n try {\n return JSON.stringify(value);\n } catch (_error) {\n return String(value);\n }\n}\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nexport function normalizeRetryCount(retryCount: unknown): number {\n if (typeof retryCount !== 'number' || !Number.isFinite(retryCount)) {\n return 1;\n }\n\n return Math.max(0, Math.floor(retryCount));\n}\n\nexport function appendAIRequestFailureSummary<T extends Error>(\n error: T,\n attemptErrors: Array<{ attempt: number; error: unknown }>,\n maxAttempts: number,\n): T {\n const failedAttempts = attemptErrors.length;\n const retries = Math.max(0, failedAttempts - 1);\n const retryLabel = retries === 1 ? 'retry' : 'retries';\n const originalMessage = error.message;\n const previousAttemptErrors = attemptErrors.slice(0, -1);\n\n error.message = `AI model request failed after ${retries} ${retryLabel} (${failedAttempts}/${maxAttempts} attempts). Last error: ${originalMessage}`;\n\n if (previousAttemptErrors.length === 0) {\n return error;\n }\n\n const details = previousAttemptErrors\n .map(\n ({ attempt, error }) => `Attempt ${attempt}: ${getErrorMessage(error)}`,\n )\n .join('\\n');\n\n error.message = `${error.message}\\nPrevious AI call attempt errors:\\n${details}`;\n return error;\n}\n\nexport const buildUsageInfo = ({\n usageData,\n requestId,\n timeCost,\n totalTimeCost,\n retryCount,\n modelName,\n modelDescription,\n responseModelName,\n slot,\n internalCallId,\n}: {\n usageData?: OpenAI.CompletionUsage;\n requestId?: string | null;\n timeCost?: number;\n totalTimeCost: number;\n retryCount: number;\n modelName: string;\n modelDescription: string;\n responseModelName?: string;\n slot: ModelRuntime['config']['slot'];\n internalCallId: string;\n}): AIUsageInfo | undefined => {\n if (!usageData) return undefined;\n\n const cachedInputTokens = (\n usageData as { prompt_tokens_details?: { cached_tokens?: number } }\n )?.prompt_tokens_details?.cached_tokens;\n\n return {\n ...usageData,\n prompt_tokens: usageData.prompt_tokens ?? 0,\n completion_tokens: usageData.completion_tokens ?? 0,\n total_tokens: usageData.total_tokens ?? 0,\n cached_input: cachedInputTokens ?? 0,\n time_cost: timeCost ?? 0,\n total_time_cost: totalTimeCost,\n retry_count: retryCount,\n model_name: modelName,\n model_description: modelDescription,\n response_model_name: responseModelName,\n slot,\n // Left undefined at the raw call layer. The agent's onUsage callback\n // fills it from modelConfig.slot for metrics collection, and task\n // layers use withUsageIntent() to stamp a more specific semantic\n // intent (e.g. 'planning', 'insight') when attaching usage to tasks.\n intent: undefined,\n request_id: requestId ?? undefined,\n // Internal stable ID for cross-path dedup when request_id is absent.\n [INTERNAL_CALL_ID_FIELD]: internalCallId,\n } satisfies AIUsageInfo;\n};\n\nexport const applyImageDetail = ({\n imageDetail,\n messages,\n}: {\n imageDetail?: ImageDetail;\n messages: ChatCompletionMessageParam[];\n}): ChatCompletionMessageParam[] => {\n if (!imageDetail) {\n return messages;\n }\n\n return messages.map((msg) => {\n if (!Array.isArray(msg.content)) {\n return msg;\n }\n\n const content = msg.content.map((part) => {\n if (part && part.type === 'image_url' && part.image_url?.url) {\n return {\n ...part,\n image_url: {\n ...part.image_url,\n detail: imageDetail,\n },\n };\n }\n return part;\n });\n\n return {\n ...msg,\n content,\n } as ChatCompletionMessageParam;\n });\n};\n\nexport const hasUsableText = (\n value: string | null | undefined,\n): value is string => typeof value === 'string' && value.trim().length > 0;\n"],"names":["AIResponseParseError","Error","message","rawResponse","usage","rawChoiceMessage","reasoningContent","INTERNAL_CALL_ID_FIELD","internalCallIdCounter","nextInternalCallId","stringifyForDebug","value","JSON","_error","String","getErrorMessage","error","toError","normalizeRetryCount","retryCount","Number","Math","appendAIRequestFailureSummary","attemptErrors","maxAttempts","failedAttempts","retries","retryLabel","originalMessage","previousAttemptErrors","details","attempt","buildUsageInfo","usageData","requestId","timeCost","totalTimeCost","modelName","modelDescription","responseModelName","slot","internalCallId","cachedInputTokens","undefined","applyImageDetail","imageDetail","messages","msg","Array","content","part","hasUsableText"],"mappings":"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"}