@midscene/core 1.12.6-beta-20260910093036.0 → 1.12.6

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 (84) hide show
  1. package/dist/es/agent/agent.mjs +12 -2
  2. package/dist/es/agent/agent.mjs.map +1 -1
  3. package/dist/es/agent/utils.mjs +1 -1
  4. package/dist/es/ai-model/service-caller/call-ai.mjs +49 -0
  5. package/dist/es/ai-model/service-caller/call-ai.mjs.map +1 -0
  6. package/dist/es/ai-model/service-caller/call.mjs +56 -0
  7. package/dist/es/ai-model/service-caller/call.mjs.map +1 -0
  8. package/dist/es/ai-model/service-caller/chat-completion/call.mjs +101 -0
  9. package/dist/es/ai-model/service-caller/chat-completion/call.mjs.map +1 -0
  10. package/dist/es/ai-model/service-caller/chat-completion/non-stream.mjs +80 -0
  11. package/dist/es/ai-model/service-caller/chat-completion/non-stream.mjs.map +1 -0
  12. package/dist/es/ai-model/service-caller/chat-completion/stream.mjs +87 -0
  13. package/dist/es/ai-model/service-caller/chat-completion/stream.mjs.map +1 -0
  14. package/dist/es/ai-model/service-caller/chat-completion/types.mjs +0 -0
  15. package/dist/es/ai-model/service-caller/chat-completion/utils.mjs +35 -0
  16. package/dist/es/ai-model/service-caller/chat-completion/utils.mjs.map +1 -0
  17. package/dist/es/ai-model/service-caller/codex/call-codex.mjs +77 -0
  18. package/dist/es/ai-model/service-caller/codex/call-codex.mjs.map +1 -0
  19. package/dist/es/ai-model/service-caller/{codex-app-server.mjs → codex/codex-app-server.mjs} +8 -17
  20. package/dist/es/ai-model/service-caller/codex/codex-app-server.mjs.map +1 -0
  21. package/dist/es/ai-model/service-caller/index.mjs +5 -582
  22. package/dist/es/ai-model/service-caller/openai-client.mjs +88 -0
  23. package/dist/es/ai-model/service-caller/openai-client.mjs.map +1 -0
  24. package/dist/es/ai-model/service-caller/proxy.mjs +66 -0
  25. package/dist/es/ai-model/service-caller/proxy.mjs.map +1 -0
  26. package/dist/es/ai-model/service-caller/utils.mjs +83 -0
  27. package/dist/es/ai-model/service-caller/utils.mjs.map +1 -0
  28. package/dist/es/report-html-template.mjs +1 -1
  29. package/dist/es/test-run-report.mjs.map +1 -1
  30. package/dist/es/types.mjs.map +1 -1
  31. package/dist/es/utils.mjs +1 -1
  32. package/dist/lib/agent/agent.js +12 -2
  33. package/dist/lib/agent/agent.js.map +1 -1
  34. package/dist/lib/agent/utils.js +1 -1
  35. package/dist/lib/ai-model/service-caller/call-ai.js +89 -0
  36. package/dist/lib/ai-model/service-caller/call-ai.js.map +1 -0
  37. package/dist/lib/ai-model/service-caller/call.js +90 -0
  38. package/dist/lib/ai-model/service-caller/call.js.map +1 -0
  39. package/dist/lib/ai-model/service-caller/chat-completion/call.js +135 -0
  40. package/dist/lib/ai-model/service-caller/chat-completion/call.js.map +1 -0
  41. package/dist/lib/ai-model/service-caller/chat-completion/non-stream.js +114 -0
  42. package/dist/lib/ai-model/service-caller/chat-completion/non-stream.js.map +1 -0
  43. package/dist/lib/ai-model/service-caller/chat-completion/stream.js +121 -0
  44. package/dist/lib/ai-model/service-caller/chat-completion/stream.js.map +1 -0
  45. package/dist/lib/ai-model/service-caller/chat-completion/types.js +20 -0
  46. package/dist/lib/ai-model/service-caller/chat-completion/types.js.map +1 -0
  47. package/dist/lib/ai-model/service-caller/chat-completion/utils.js +75 -0
  48. package/dist/lib/ai-model/service-caller/chat-completion/utils.js.map +1 -0
  49. package/dist/lib/ai-model/service-caller/codex/call-codex.js +111 -0
  50. package/dist/lib/ai-model/service-caller/codex/call-codex.js.map +1 -0
  51. package/dist/lib/ai-model/service-caller/{codex-app-server.js → codex/codex-app-server.js} +8 -17
  52. package/dist/lib/ai-model/service-caller/codex/codex-app-server.js.map +1 -0
  53. package/dist/lib/ai-model/service-caller/index.js +11 -596
  54. package/dist/lib/ai-model/service-caller/index.js.map +1 -1
  55. package/dist/lib/ai-model/service-caller/openai-client.js +132 -0
  56. package/dist/lib/ai-model/service-caller/openai-client.js.map +1 -0
  57. package/dist/lib/ai-model/service-caller/proxy.js +100 -0
  58. package/dist/lib/ai-model/service-caller/proxy.js.map +1 -0
  59. package/dist/lib/ai-model/service-caller/utils.js +144 -0
  60. package/dist/lib/ai-model/service-caller/utils.js.map +1 -0
  61. package/dist/lib/report-html-template.js +1 -1
  62. package/dist/lib/test-run-report.js.map +1 -1
  63. package/dist/lib/types.js.map +1 -1
  64. package/dist/lib/utils.js +1 -1
  65. package/dist/types/ai-model/service-caller/call-ai.d.ts +26 -0
  66. package/dist/types/ai-model/service-caller/call.d.ts +4 -0
  67. package/dist/types/ai-model/service-caller/chat-completion/call.d.ts +2 -0
  68. package/dist/types/ai-model/service-caller/chat-completion/non-stream.d.ts +2 -0
  69. package/dist/types/ai-model/service-caller/chat-completion/stream.d.ts +2 -0
  70. package/dist/types/ai-model/service-caller/chat-completion/types.d.ts +25 -0
  71. package/dist/types/ai-model/service-caller/chat-completion/utils.d.ts +12 -0
  72. package/dist/types/ai-model/service-caller/codex/call-codex.d.ts +2 -0
  73. package/dist/types/ai-model/service-caller/{codex-app-server.d.ts → codex/codex-app-server.d.ts} +4 -3
  74. package/dist/types/ai-model/service-caller/index.d.ts +4 -74
  75. package/dist/types/ai-model/service-caller/openai-client.d.ts +14 -0
  76. package/dist/types/ai-model/service-caller/proxy.d.ts +4 -0
  77. package/dist/types/ai-model/service-caller/types.d.ts +35 -0
  78. package/dist/types/ai-model/service-caller/utils.d.ts +40 -0
  79. package/dist/types/test-run-report.d.ts +0 -1
  80. package/dist/types/types.d.ts +3 -2
  81. package/package.json +4 -4
  82. package/dist/es/ai-model/service-caller/codex-app-server.mjs.map +0 -1
  83. package/dist/es/ai-model/service-caller/index.mjs.map +0 -1
  84. package/dist/lib/ai-model/service-caller/codex-app-server.js.map +0 -1
@@ -1,583 +1,6 @@
1
- import { MIDSCENE_LANGFUSE_DEBUG, MIDSCENE_LANGSMITH_DEBUG, globalConfigManager } from "@midscene/shared/env";
2
- import { getDebug } from "@midscene/shared/logger";
3
- import { assert, ifInBrowser, uuid } from "@midscene/shared/utils";
4
- import openai_0 from "openai";
5
- import { getVersion } from "../../utils.mjs";
6
- import { assertJsonObject, extractJSONFromCodeBlock, parseModelResponseJson } from "../shared/json.mjs";
7
- import { callAIWithCodexAppServer, isCodexAppServerProvider } from "./codex-app-server.mjs";
8
- import { isModelCallRecordingEnabled, recordModelCallEvent } from "./model-call-recorder.mjs";
9
- import { formatOpenAIAPIErrorDetails, wrapOpenAICompatibleFetch } from "./openai-error.mjs";
10
- import { buildRequestAbortSignal, isHardTimeoutError, resolveEffectiveTimeoutMs, restoreHardTimeoutError } from "./request-timeout.mjs";
11
- import { callAiAndParseWithRetry, withSemanticRetryFeedback } from "./semantic-retry.mjs";
12
- function _define_property(obj, key, value) {
13
- if (key in obj) Object.defineProperty(obj, key, {
14
- value: value,
15
- enumerable: true,
16
- configurable: true,
17
- writable: true
18
- });
19
- else obj[key] = value;
20
- return obj;
21
- }
22
- class AIResponseParseError extends Error {
23
- constructor(message, rawResponse, usage, rawChoiceMessage, reasoningContent){
24
- super(message), _define_property(this, "usage", void 0), _define_property(this, "rawResponse", void 0), _define_property(this, "rawChoiceMessage", void 0), _define_property(this, "reasoningContent", void 0);
25
- this.name = 'AIResponseParseError';
26
- this.rawResponse = rawResponse;
27
- this.usage = usage;
28
- this.rawChoiceMessage = rawChoiceMessage;
29
- this.reasoningContent = reasoningContent;
30
- }
31
- }
32
- const INTERNAL_CALL_ID_FIELD = '_midscene_call_id';
33
- let internalCallIdCounter = 0;
34
- function nextInternalCallId() {
35
- internalCallIdCounter += 1;
36
- return `call_${internalCallIdCounter}`;
37
- }
38
- function stringifyForDebug(value) {
39
- try {
40
- return JSON.stringify(value);
41
- } catch (_error) {
42
- return String(value);
43
- }
44
- }
45
- function getLatestSuccessfulResponseRequestId(context) {
46
- return context.responseRequestIds?.reduce((latestRequestId, response)=>response.ok ? response.requestId : latestRequestId, void 0);
47
- }
48
- function getLatestResponseAttempt(context) {
49
- return context.httpResponses?.at(-1)?.attempt ?? 1;
50
- }
51
- function getErrorMessage(error) {
52
- return error instanceof Error ? error.message : String(error);
53
- }
54
- function toError(error) {
55
- return error instanceof Error ? error : new Error(String(error));
56
- }
57
- function normalizeRetryCount(retryCount) {
58
- if ('number' != typeof retryCount || !Number.isFinite(retryCount)) return 1;
59
- return Math.max(0, Math.floor(retryCount));
60
- }
61
- function appendAIRequestFailureSummary(error, attemptErrors, maxAttempts) {
62
- const failedAttempts = attemptErrors.length;
63
- const retries = Math.max(0, failedAttempts - 1);
64
- const retryLabel = 1 === retries ? 'retry' : 'retries';
65
- const originalMessage = error.message;
66
- const previousAttemptErrors = attemptErrors.slice(0, -1);
67
- error.message = `AI model request failed after ${retries} ${retryLabel} (${failedAttempts}/${maxAttempts} attempts). Last error: ${originalMessage}`;
68
- if (0 === previousAttemptErrors.length) return error;
69
- const details = previousAttemptErrors.map(({ attempt, error })=>`Attempt ${attempt}: ${getErrorMessage(error)}`).join('\n');
70
- error.message = `${error.message}\nPrevious AI call attempt errors:\n${details}`;
71
- return error;
72
- }
73
- async function createChatClient({ modelConfig, executionId, recordEvent }) {
74
- const { socksProxy, httpProxy, modelName, openaiBaseURL, openaiApiKey, openaiExtraConfig, modelDescription, modelFamily, createOpenAIClient, timeout } = modelConfig;
75
- let proxyAgent;
76
- const warnClient = getDebug('ai:call', {
77
- console: true
78
- });
79
- const debugProxy = getDebug('ai:call:proxy');
80
- const warnProxy = getDebug('ai:call:proxy', {
81
- console: true
82
- });
83
- const sanitizeProxyUrl = (url)=>{
84
- try {
85
- const parsed = new URL(url);
86
- if (parsed.username) {
87
- parsed.password = '****';
88
- return parsed.href;
89
- }
90
- return url;
91
- } catch {
92
- return url;
93
- }
94
- };
95
- if (httpProxy) {
96
- debugProxy('using http proxy', sanitizeProxyUrl(httpProxy));
97
- if (ifInBrowser) warnProxy('HTTP proxy is configured but not supported in browser environment');
98
- else {
99
- const { loadUndici } = await import("#proxy-deps");
100
- const { ProxyAgent } = await loadUndici();
101
- proxyAgent = new ProxyAgent({
102
- uri: httpProxy
103
- });
104
- }
105
- } else if (socksProxy) {
106
- debugProxy('using socks proxy', sanitizeProxyUrl(socksProxy));
107
- if (ifInBrowser) warnProxy('SOCKS proxy is configured but not supported in browser environment');
108
- else try {
109
- const { loadFetchSocks } = await import("#proxy-deps");
110
- const { socksDispatcher } = await loadFetchSocks();
111
- const proxyUrl = new URL(socksProxy);
112
- if (!proxyUrl.hostname) throw new Error('SOCKS proxy URL must include a valid hostname');
113
- const port = Number.parseInt(proxyUrl.port, 10);
114
- if (!proxyUrl.port || Number.isNaN(port)) throw new Error('SOCKS proxy URL must include a valid port');
115
- const protocol = proxyUrl.protocol.replace(':', '');
116
- const socksType = 'socks4' === protocol ? 4 : 'socks5' === protocol ? 5 : 5;
117
- proxyAgent = socksDispatcher({
118
- type: socksType,
119
- host: proxyUrl.hostname,
120
- port,
121
- ...proxyUrl.username ? {
122
- userId: decodeURIComponent(proxyUrl.username),
123
- password: decodeURIComponent(proxyUrl.password || '')
124
- } : {}
125
- });
126
- debugProxy('socks proxy configured successfully', {
127
- type: socksType,
128
- host: proxyUrl.hostname,
129
- port: port
130
- });
131
- } catch (error) {
132
- warnProxy('Failed to configure SOCKS proxy:', error);
133
- throw new Error(`Invalid SOCKS proxy URL: ${socksProxy}. Expected format: socks4://host:port, socks5://host:port, or with authentication: socks5://user:pass@host:port`);
134
- }
135
- }
136
- const effectiveTimeoutMs = resolveEffectiveTimeoutMs({
137
- timeout
138
- });
139
- const openAIErrorResponseContext = {
140
- recordEvent
141
- };
142
- const openAIOptions = {
143
- baseURL: openaiBaseURL,
144
- apiKey: openaiApiKey,
145
- ...proxyAgent ? {
146
- fetchOptions: {
147
- dispatcher: proxyAgent
148
- }
149
- } : {},
150
- ...openaiExtraConfig,
151
- defaultHeaders: {
152
- ...openaiExtraConfig?.defaultHeaders,
153
- 'x-midscene-version': getVersion(),
154
- 'x-midscene-execution-id': executionId
155
- },
156
- fetch: wrapOpenAICompatibleFetch(openAIErrorResponseContext),
157
- maxRetries: 0,
158
- ...null !== effectiveTimeoutMs ? {
159
- timeout: effectiveTimeoutMs
160
- } : {},
161
- dangerouslyAllowBrowser: true
162
- };
163
- const baseOpenAI = new openai_0(openAIOptions);
164
- let openai = baseOpenAI;
165
- if (openai && globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGSMITH_DEBUG)) {
166
- if (ifInBrowser) throw new Error('langsmith is not supported in browser');
167
- warnClient('DEBUGGING MODE: langsmith wrapper enabled');
168
- const langsmithModule = 'langsmith/wrappers';
169
- const { wrapOpenAI } = await import(langsmithModule);
170
- openai = wrapOpenAI(openai);
171
- }
172
- if (openai && globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGFUSE_DEBUG)) {
173
- if (ifInBrowser) throw new Error('langfuse is not supported in browser');
174
- warnClient('DEBUGGING MODE: langfuse wrapper enabled');
175
- const langfuseModule = '@langfuse/openai';
176
- const { observeOpenAI } = await import(langfuseModule);
177
- openai = observeOpenAI(openai);
178
- }
179
- if (createOpenAIClient) {
180
- const wrappedClient = await createOpenAIClient(baseOpenAI, openAIOptions);
181
- if (wrappedClient) openai = wrappedClient;
182
- }
183
- return {
184
- completion: openai.chat.completions,
185
- modelName,
186
- modelDescription,
187
- modelFamily,
188
- openAIErrorResponseContext
189
- };
190
- }
191
- async function callAI(messages, modelRuntime, options) {
192
- const { config: modelConfig, adapter } = modelRuntime;
193
- const executionId = modelRuntime.executionId ?? `unscoped-${uuid()}`;
194
- const internalCallId = nextInternalCallId();
195
- const recordEvent = isModelCallRecordingEnabled() ? (event)=>{
196
- recordModelCallEvent({
197
- executionId,
198
- callId: internalCallId,
199
- semanticRetryAttempt: options?.semanticRetryAttempt,
200
- slot: modelConfig.slot,
201
- intent: modelConfig.intent,
202
- modelFamily: modelConfig.modelFamily,
203
- ...event
204
- });
205
- } : void 0;
206
- const modelCallInput = {
207
- intent: modelConfig.intent,
208
- userConfig: {
209
- temperature: modelConfig.temperature,
210
- reasoningEnabled: modelConfig.reasoningEnabled,
211
- reasoningEffort: modelConfig.reasoningEffort,
212
- reasoningBudget: modelConfig.reasoningBudget,
213
- responseFormat: modelConfig.responseFormat
214
- },
215
- semanticRetryAttempt: options?.semanticRetryAttempt,
216
- requiresOriginalImageDetail: options?.requiresOriginalImageDetail,
217
- expectedJsonObjectResponse: options?.expectedJsonObjectResponse
218
- };
219
- if (isCodexAppServerProvider(modelConfig.openaiBaseURL)) {
220
- let protocolChunkSequence = 0;
221
- const codexStartTime = Date.now();
222
- const recordCodexEvent = recordEvent ? (event)=>{
223
- if ('chunk' === event.type) {
224
- protocolChunkSequence += 1;
225
- recordEvent({
226
- ...event,
227
- attempt: 1,
228
- sequence: protocolChunkSequence,
229
- provider: 'codex-app-server'
230
- });
231
- return;
232
- }
233
- recordEvent({
234
- ...event,
235
- attempt: 1,
236
- provider: 'codex-app-server'
237
- });
238
- } : void 0;
239
- try {
240
- const { config, imageDetail } = adapter.buildCodexAppServerParams(modelCallInput);
241
- const codexResult = await callAIWithCodexAppServer(messages, modelConfig, {
242
- stream: options?.stream,
243
- onChunk: options?.onChunk,
244
- params: config,
245
- abortSignal: options?.abortSignal,
246
- imageDetail,
247
- onRecordEvent: recordCodexEvent
248
- });
249
- const { protocolMetadata, ...response } = codexResult;
250
- recordEvent?.({
251
- type: 'response',
252
- attempt: 1,
253
- provider: 'codex-app-server',
254
- final: {
255
- content: response.content,
256
- reasoningContent: response.reasoning_content,
257
- usage: response.usage,
258
- timeCost: Date.now() - codexStartTime,
259
- protocol: protocolMetadata
260
- }
261
- });
262
- if (response.usage) {
263
- response.usage[INTERNAL_CALL_ID_FIELD] = internalCallId;
264
- if (modelRuntime.onUsage) modelRuntime.onUsage(response.usage);
265
- }
266
- return {
267
- ...response
268
- };
269
- } catch (error) {
270
- recordEvent?.({
271
- type: 'error',
272
- attempt: 1,
273
- provider: 'codex-app-server',
274
- error: error instanceof Error ? {
275
- name: error.name,
276
- message: error.message,
277
- stack: error.stack
278
- } : String(error)
279
- });
280
- throw error;
281
- }
282
- }
283
- const imageDetail = adapter.chatCompletion.resolveImageDetail(modelCallInput);
284
- const { completion, modelName, modelDescription, modelFamily, openAIErrorResponseContext } = await createChatClient({
285
- modelConfig,
286
- executionId,
287
- recordEvent
288
- });
289
- const effectiveTimeoutMs = resolveEffectiveTimeoutMs(modelConfig);
290
- const extraBody = modelConfig.extraBody;
291
- const debugCall = getDebug('ai:call');
292
- const warnCall = getDebug('ai:call', {
293
- console: true
294
- });
295
- const debugProfileStats = getDebug('ai:profile:stats');
296
- const debugProfileDetail = getDebug('ai:profile:detail');
297
- const startTime = Date.now();
298
- const isStreaming = options?.stream && options?.onChunk;
299
- const { config: adapterChatCompletionParams } = adapter.chatCompletion.buildChatCompletionParams(modelCallInput);
300
- debugCall(`adapter chat completion params: ${stringifyForDebug({
301
- config: adapterChatCompletionParams
302
- })}`);
303
- let content;
304
- let accumulated = '';
305
- let accumulatedReasoning = '';
306
- let rawChoiceMessage;
307
- let usage;
308
- let timeCost;
309
- let requestId;
310
- let responseModelName;
311
- let usageReported = false;
312
- const hasUsableText = (value)=>'string' == typeof value && value.trim().length > 0;
313
- const resolveContentWithReasoningFallback = (contentValue, reasoningContent)=>{
314
- if (!hasUsableText(contentValue) && adapter.chatCompletion.useReasoningAsContentFallback && hasUsableText(reasoningContent)) {
315
- warnCall('empty content from AI model, using reasoning content');
316
- return reasoningContent;
317
- }
318
- return contentValue;
319
- };
320
- const buildUsageInfo = (usageData, requestId)=>{
321
- if (!usageData) return;
322
- const cachedInputTokens = usageData?.prompt_tokens_details?.cached_tokens;
323
- return {
324
- ...usageData,
325
- prompt_tokens: usageData.prompt_tokens ?? 0,
326
- completion_tokens: usageData.completion_tokens ?? 0,
327
- total_tokens: usageData.total_tokens ?? 0,
328
- cached_input: cachedInputTokens ?? 0,
329
- time_cost: timeCost ?? 0,
330
- model_name: modelName,
331
- model_description: modelDescription,
332
- response_model_name: responseModelName,
333
- slot: modelConfig.slot,
334
- intent: void 0,
335
- request_id: requestId ?? void 0,
336
- [INTERNAL_CALL_ID_FIELD]: internalCallId
337
- };
338
- };
339
- const requestConfig = {
340
- ...adapterChatCompletionParams,
341
- ...extraBody ?? {}
342
- };
343
- const temperature = requestConfig.temperature;
344
- const messagesWithImageDetail = (()=>{
345
- if (!imageDetail) return messages;
346
- return messages.map((msg)=>{
347
- if (!Array.isArray(msg.content)) return msg;
348
- const content = msg.content.map((part)=>{
349
- if (part && 'image_url' === part.type && part.image_url?.url) return {
350
- ...part,
351
- image_url: {
352
- ...part.image_url,
353
- detail: imageDetail
354
- }
355
- };
356
- return part;
357
- });
358
- return {
359
- ...msg,
360
- content
361
- };
362
- });
363
- })();
364
- try {
365
- debugCall(`sending ${isStreaming ? 'streaming ' : ''}request to ${modelName}`);
366
- if (isStreaming) {
367
- const { signal: streamSignal, cleanup: cleanupStreamSignal } = buildRequestAbortSignal(effectiveTimeoutMs, options?.abortSignal);
368
- try {
369
- const stream = await completion.create({
370
- model: modelName,
371
- messages: messagesWithImageDetail,
372
- ...requestConfig,
373
- stream: true
374
- }, {
375
- stream: true,
376
- signal: streamSignal
377
- });
378
- requestId = getLatestSuccessfulResponseRequestId(openAIErrorResponseContext) ?? stream._request_id;
379
- const streamAttempt = getLatestResponseAttempt(openAIErrorResponseContext);
380
- let chunkSequence = 0;
381
- for await (const chunk of stream){
382
- chunkSequence += 1;
383
- recordEvent?.({
384
- type: 'chunk',
385
- attempt: streamAttempt,
386
- sequence: chunkSequence,
387
- chunk
388
- });
389
- const parsedChunk = adapter.chatCompletion.extractContentAndReasoning(chunk.choices?.[0]?.delta);
390
- const content = parsedChunk.content || '';
391
- const reasoning_content = parsedChunk.reasoning_content || '';
392
- if (chunk.usage) usage = chunk.usage;
393
- if (chunk.model) responseModelName = chunk.model;
394
- if (content || reasoning_content) {
395
- accumulated += content;
396
- accumulatedReasoning += reasoning_content;
397
- const chunkData = {
398
- content,
399
- reasoning_content,
400
- accumulated,
401
- isComplete: false,
402
- usage: void 0
403
- };
404
- options.onChunk(chunkData);
405
- }
406
- if (chunk.choices?.[0]?.finish_reason) {
407
- timeCost = Date.now() - startTime;
408
- if (!usage) {
409
- const estimatedTokens = Math.max(1, Math.floor(accumulated.length / 4));
410
- usage = {
411
- prompt_tokens: estimatedTokens,
412
- completion_tokens: estimatedTokens,
413
- total_tokens: 2 * estimatedTokens
414
- };
415
- }
416
- const finalAccumulated = resolveContentWithReasoningFallback(accumulated, accumulatedReasoning);
417
- accumulated = finalAccumulated || '';
418
- const finalUsage = buildUsageInfo(usage, requestId);
419
- if (finalUsage && modelRuntime.onUsage) {
420
- modelRuntime.onUsage(finalUsage);
421
- usageReported = true;
422
- }
423
- const finalChunk = {
424
- content: '',
425
- accumulated,
426
- reasoning_content: '',
427
- isComplete: true,
428
- usage: finalUsage
429
- };
430
- options.onChunk(finalChunk);
431
- break;
432
- }
433
- }
434
- } catch (error) {
435
- throw restoreHardTimeoutError(toError(error), streamSignal);
436
- } finally{
437
- cleanupStreamSignal();
438
- }
439
- content = accumulated;
440
- debugProfileStats(`streaming model, ${modelName}, mode, ${modelFamily || 'default'}, cost-ms, ${timeCost}, temperature, ${temperature ?? ''}`);
441
- } else {
442
- const retryCount = normalizeRetryCount(modelConfig.retryCount);
443
- const retryInterval = modelConfig.retryInterval ?? 2000;
444
- const maxAttempts = retryCount + 1;
445
- let lastError;
446
- const attemptErrors = [];
447
- for(let attempt = 1; attempt <= maxAttempts; attempt++){
448
- const { signal: attemptSignal, cleanup: cleanupAttemptSignal } = buildRequestAbortSignal(effectiveTimeoutMs, options?.abortSignal);
449
- try {
450
- const result = await completion.create({
451
- model: modelName,
452
- messages: messagesWithImageDetail,
453
- ...requestConfig,
454
- stream: false
455
- }, {
456
- signal: attemptSignal
457
- });
458
- timeCost = Date.now() - startTime;
459
- requestId = getLatestSuccessfulResponseRequestId(openAIErrorResponseContext) ?? result._request_id;
460
- debugProfileStats(`model, ${modelName}, mode, ${modelFamily || 'default'}, prompt-tokens, ${result.usage?.prompt_tokens || ''}, completion-tokens, ${result.usage?.completion_tokens || ''}, total-tokens, ${result.usage?.total_tokens || ''}, cost-ms, ${timeCost}, requestId, ${requestId || ''}, temperature, ${temperature ?? ''}`);
461
- debugProfileDetail(`model usage detail: ${JSON.stringify(result.usage)}`);
462
- if (!result.choices) throw new Error(`invalid response from LLM service: ${JSON.stringify(result)}`);
463
- rawChoiceMessage = result.choices[0].message;
464
- const parsedMessage = adapter.chatCompletion.extractContentAndReasoning(result.choices[0].message);
465
- content = parsedMessage.content;
466
- accumulatedReasoning = parsedMessage.reasoning_content;
467
- usage = result.usage;
468
- responseModelName = result.model;
469
- content = resolveContentWithReasoningFallback(content, accumulatedReasoning);
470
- if (!hasUsableText(content)) {
471
- const errorUsage = buildUsageInfo(usage, requestId);
472
- if (errorUsage && modelRuntime.onUsage) modelRuntime.onUsage(errorUsage);
473
- throw new AIResponseParseError('empty content from AI model', content || '', errorUsage, rawChoiceMessage);
474
- }
475
- break;
476
- } catch (error) {
477
- lastError = restoreHardTimeoutError(toError(error), attemptSignal);
478
- attemptErrors.push({
479
- attempt,
480
- error: lastError
481
- });
482
- const wasHardTimeout = isHardTimeoutError(lastError);
483
- if (wasHardTimeout) warnCall(`AI call hit hard timeout (${effectiveTimeoutMs}ms, attempt ${attempt}/${maxAttempts}, model ${modelName}, slot ${modelConfig.slot})`);
484
- if (options?.abortSignal?.aborted) break;
485
- if (attempt < maxAttempts) {
486
- warnCall(`AI call failed (attempt ${attempt}/${maxAttempts}), retrying in ${retryInterval}ms... Error: ${lastError.message}`);
487
- await new Promise((resolve)=>setTimeout(resolve, retryInterval));
488
- }
489
- } finally{
490
- cleanupAttemptSignal();
491
- }
492
- }
493
- if (!content) {
494
- assert(lastError, 'AI model request failed without recording an attempt error');
495
- throw appendAIRequestFailureSummary(lastError, attemptErrors, maxAttempts);
496
- }
497
- }
498
- debugCall(`response reasoning content: ${accumulatedReasoning}`);
499
- debugCall(`response content: ${content}`);
500
- if (isStreaming && !usage) {
501
- const estimatedTokens = Math.max(1, Math.floor((content || '').length / 4));
502
- usage = {
503
- prompt_tokens: estimatedTokens,
504
- completion_tokens: estimatedTokens,
505
- total_tokens: 2 * estimatedTokens
506
- };
507
- }
508
- const finalUsage = buildUsageInfo(usage, requestId);
509
- if (!usageReported && finalUsage && modelRuntime.onUsage) modelRuntime.onUsage(finalUsage);
510
- const response = {
511
- content: content || '',
512
- reasoning_content: accumulatedReasoning || void 0,
513
- rawChoiceMessage,
514
- usage: finalUsage,
515
- isStreamed: !!isStreaming
516
- };
517
- recordEvent?.({
518
- type: 'response',
519
- attempt: getLatestResponseAttempt(openAIErrorResponseContext),
520
- http: openAIErrorResponseContext.httpResponses?.at(-1),
521
- final: {
522
- content: response.content,
523
- reasoningContent: response.reasoning_content,
524
- usage: response.usage,
525
- requestId,
526
- timeCost,
527
- responseModelName
528
- }
529
- });
530
- return response;
531
- } catch (e) {
532
- warnCall('call AI error', e);
533
- if (e instanceof AIResponseParseError) throw e;
534
- const newError = new Error(`failed to call ${isStreaming ? 'streaming ' : ''}AI model service (${modelName}): ${e.message}${formatOpenAIAPIErrorDetails(e, openAIErrorResponseContext)}\nTrouble shooting: https://midscenejs.com/model-provider.html`, {
535
- cause: e
536
- });
537
- throw newError;
538
- }
539
- }
540
- function parseAIObjectResponse(response, modelRuntime, jsonParserSource = 'generic-object') {
541
- const { adapter } = modelRuntime;
542
- assert(response, 'empty response');
543
- const jsonContent = adapter.jsonParser(response.content, {
544
- source: jsonParserSource
545
- });
546
- assertJsonObject(jsonContent);
547
- return {
548
- content: jsonContent,
549
- contentString: response.content,
550
- usage: response.usage,
551
- reasoning_content: response.reasoning_content,
552
- rawChoiceMessage: response.rawChoiceMessage
553
- };
554
- }
555
- async function callAIWithObjectResponse(messages, modelRuntime, options) {
556
- const { config: modelConfig } = modelRuntime;
557
- return callAiAndParseWithRetry({
558
- callAi: (retryAttempt, previousParseError)=>callAI(withSemanticRetryFeedback(messages, previousParseError), modelRuntime, {
559
- abortSignal: options?.abortSignal,
560
- expectedJsonObjectResponse: true,
561
- semanticRetryAttempt: retryAttempt
562
- }),
563
- parseResponse: (response)=>parseAIObjectResponse(response, modelRuntime, options?.jsonParserSource),
564
- toParseError: (error, response)=>{
565
- const errorMessage = error instanceof Error ? error.message : String(error);
566
- return new AIResponseParseError(errorMessage, response.content, response.usage, response.rawChoiceMessage, response.reasoning_content);
567
- },
568
- parseRetryTimes: options?.retryTimes ?? modelConfig.retryCount,
569
- parseRetryInterval: options?.retryInterval ?? modelConfig.retryInterval,
570
- abortSignal: options?.abortSignal
571
- });
572
- }
573
- async function callAIWithStringResponse(msgs, modelRuntime, options) {
574
- const { content, usage, rawChoiceMessage } = await callAI(msgs, modelRuntime, options);
575
- return {
576
- content,
577
- usage,
578
- rawChoiceMessage
579
- };
580
- }
1
+ import { callAIWithObjectResponse, callAIWithStringResponse, parseAIObjectResponse } from "./call-ai.mjs";
2
+ import { callAI } from "./call.mjs";
3
+ import { createChatClient } from "./openai-client.mjs";
4
+ import { AIResponseParseError, INTERNAL_CALL_ID_FIELD } from "./utils.mjs";
5
+ import { extractJSONFromCodeBlock, parseModelResponseJson } from "../shared/json.mjs";
581
6
  export { AIResponseParseError, INTERNAL_CALL_ID_FIELD, callAI, callAIWithObjectResponse, callAIWithStringResponse, createChatClient, extractJSONFromCodeBlock, parseAIObjectResponse, parseModelResponseJson };
582
-
583
- //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,88 @@
1
+ import { MIDSCENE_LANGFUSE_DEBUG, MIDSCENE_LANGSMITH_DEBUG, globalConfigManager } from "@midscene/shared/env";
2
+ import { getDebug } from "@midscene/shared/logger";
3
+ import { ifInBrowser } from "@midscene/shared/utils";
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 })=>{
10
+ const warnClient = getDebug('ai:call', {
11
+ console: true
12
+ });
13
+ const openAIOptions = {
14
+ baseURL: openaiBaseURL,
15
+ apiKey: openaiApiKey,
16
+ ...proxyAgent ? {
17
+ fetchOptions: {
18
+ dispatcher: proxyAgent
19
+ }
20
+ } : {},
21
+ ...openaiExtraConfig,
22
+ defaultHeaders: {
23
+ ...openaiExtraConfig?.defaultHeaders,
24
+ 'x-midscene-version': getVersion(),
25
+ 'x-midscene-execution-id': executionId
26
+ },
27
+ fetch: wrapOpenAICompatibleFetch(openAIErrorResponseContext),
28
+ maxRetries: 0,
29
+ ...null !== effectiveTimeoutMs ? {
30
+ timeout: effectiveTimeoutMs
31
+ } : {},
32
+ dangerouslyAllowBrowser: true
33
+ };
34
+ const baseOpenAI = new openai_0(openAIOptions);
35
+ let openai = baseOpenAI;
36
+ if (openai && globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGSMITH_DEBUG)) {
37
+ if (ifInBrowser) throw new Error('langsmith is not supported in browser');
38
+ warnClient('DEBUGGING MODE: langsmith wrapper enabled');
39
+ const langsmithModule = 'langsmith/wrappers';
40
+ const { wrapOpenAI } = await import(langsmithModule);
41
+ openai = wrapOpenAI(openai);
42
+ }
43
+ if (openai && globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGFUSE_DEBUG)) {
44
+ if (ifInBrowser) throw new Error('langfuse is not supported in browser');
45
+ warnClient('DEBUGGING MODE: langfuse wrapper enabled');
46
+ const langfuseModule = '@langfuse/openai';
47
+ const { observeOpenAI } = await import(langfuseModule);
48
+ openai = observeOpenAI(openai);
49
+ }
50
+ if (createOpenAIClient) {
51
+ const wrappedClient = await createOpenAIClient(baseOpenAI, openAIOptions);
52
+ if (wrappedClient) openai = wrappedClient;
53
+ }
54
+ return openai;
55
+ };
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 = {
66
+ recordEvent
67
+ };
68
+ const openai = await createAndWrapClient({
69
+ openaiBaseURL,
70
+ openaiApiKey,
71
+ openaiExtraConfig,
72
+ createOpenAIClient,
73
+ effectiveTimeoutMs,
74
+ proxyAgent,
75
+ executionId,
76
+ openAIErrorResponseContext
77
+ });
78
+ return {
79
+ completion: openai.chat.completions,
80
+ modelName,
81
+ modelDescription,
82
+ modelFamily,
83
+ openAIErrorResponseContext
84
+ };
85
+ }
86
+ export { createChatClient };
87
+
88
+ //# sourceMappingURL=openai-client.mjs.map