@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.
- package/dist/es/agent/agent.mjs +12 -2
- package/dist/es/agent/agent.mjs.map +1 -1
- package/dist/es/agent/utils.mjs +1 -1
- package/dist/es/ai-model/service-caller/call-ai.mjs +49 -0
- package/dist/es/ai-model/service-caller/call-ai.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/call.mjs +56 -0
- package/dist/es/ai-model/service-caller/call.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/chat-completion/call.mjs +101 -0
- package/dist/es/ai-model/service-caller/chat-completion/call.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/chat-completion/non-stream.mjs +80 -0
- package/dist/es/ai-model/service-caller/chat-completion/non-stream.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/chat-completion/stream.mjs +87 -0
- package/dist/es/ai-model/service-caller/chat-completion/stream.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/chat-completion/types.mjs +0 -0
- package/dist/es/ai-model/service-caller/chat-completion/utils.mjs +35 -0
- package/dist/es/ai-model/service-caller/chat-completion/utils.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/codex/call-codex.mjs +77 -0
- package/dist/es/ai-model/service-caller/codex/call-codex.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/{codex-app-server.mjs → codex/codex-app-server.mjs} +8 -17
- package/dist/es/ai-model/service-caller/codex/codex-app-server.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/index.mjs +5 -582
- package/dist/es/ai-model/service-caller/openai-client.mjs +88 -0
- package/dist/es/ai-model/service-caller/openai-client.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/proxy.mjs +66 -0
- package/dist/es/ai-model/service-caller/proxy.mjs.map +1 -0
- package/dist/es/ai-model/service-caller/utils.mjs +83 -0
- package/dist/es/ai-model/service-caller/utils.mjs.map +1 -0
- package/dist/es/report-html-template.mjs +1 -1
- package/dist/es/test-run-report.mjs.map +1 -1
- package/dist/es/types.mjs.map +1 -1
- package/dist/es/utils.mjs +1 -1
- package/dist/lib/agent/agent.js +12 -2
- package/dist/lib/agent/agent.js.map +1 -1
- package/dist/lib/agent/utils.js +1 -1
- package/dist/lib/ai-model/service-caller/call-ai.js +89 -0
- package/dist/lib/ai-model/service-caller/call-ai.js.map +1 -0
- package/dist/lib/ai-model/service-caller/call.js +90 -0
- package/dist/lib/ai-model/service-caller/call.js.map +1 -0
- package/dist/lib/ai-model/service-caller/chat-completion/call.js +135 -0
- package/dist/lib/ai-model/service-caller/chat-completion/call.js.map +1 -0
- package/dist/lib/ai-model/service-caller/chat-completion/non-stream.js +114 -0
- package/dist/lib/ai-model/service-caller/chat-completion/non-stream.js.map +1 -0
- package/dist/lib/ai-model/service-caller/chat-completion/stream.js +121 -0
- package/dist/lib/ai-model/service-caller/chat-completion/stream.js.map +1 -0
- package/dist/lib/ai-model/service-caller/chat-completion/types.js +20 -0
- package/dist/lib/ai-model/service-caller/chat-completion/types.js.map +1 -0
- package/dist/lib/ai-model/service-caller/chat-completion/utils.js +75 -0
- package/dist/lib/ai-model/service-caller/chat-completion/utils.js.map +1 -0
- package/dist/lib/ai-model/service-caller/codex/call-codex.js +111 -0
- package/dist/lib/ai-model/service-caller/codex/call-codex.js.map +1 -0
- package/dist/lib/ai-model/service-caller/{codex-app-server.js → codex/codex-app-server.js} +8 -17
- package/dist/lib/ai-model/service-caller/codex/codex-app-server.js.map +1 -0
- package/dist/lib/ai-model/service-caller/index.js +11 -596
- package/dist/lib/ai-model/service-caller/index.js.map +1 -1
- package/dist/lib/ai-model/service-caller/openai-client.js +132 -0
- package/dist/lib/ai-model/service-caller/openai-client.js.map +1 -0
- package/dist/lib/ai-model/service-caller/proxy.js +100 -0
- package/dist/lib/ai-model/service-caller/proxy.js.map +1 -0
- package/dist/lib/ai-model/service-caller/utils.js +144 -0
- package/dist/lib/ai-model/service-caller/utils.js.map +1 -0
- package/dist/lib/report-html-template.js +1 -1
- package/dist/lib/test-run-report.js.map +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/types/ai-model/service-caller/call-ai.d.ts +26 -0
- package/dist/types/ai-model/service-caller/call.d.ts +4 -0
- package/dist/types/ai-model/service-caller/chat-completion/call.d.ts +2 -0
- package/dist/types/ai-model/service-caller/chat-completion/non-stream.d.ts +2 -0
- package/dist/types/ai-model/service-caller/chat-completion/stream.d.ts +2 -0
- package/dist/types/ai-model/service-caller/chat-completion/types.d.ts +25 -0
- package/dist/types/ai-model/service-caller/chat-completion/utils.d.ts +12 -0
- package/dist/types/ai-model/service-caller/codex/call-codex.d.ts +2 -0
- package/dist/types/ai-model/service-caller/{codex-app-server.d.ts → codex/codex-app-server.d.ts} +4 -3
- package/dist/types/ai-model/service-caller/index.d.ts +4 -74
- package/dist/types/ai-model/service-caller/openai-client.d.ts +14 -0
- package/dist/types/ai-model/service-caller/proxy.d.ts +4 -0
- package/dist/types/ai-model/service-caller/types.d.ts +35 -0
- package/dist/types/ai-model/service-caller/utils.d.ts +40 -0
- package/dist/types/test-run-report.d.ts +0 -1
- package/dist/types/types.d.ts +3 -2
- package/package.json +4 -4
- package/dist/es/ai-model/service-caller/codex-app-server.mjs.map +0 -1
- package/dist/es/ai-model/service-caller/index.mjs.map +0 -1
- package/dist/lib/ai-model/service-caller/codex-app-server.js.map +0 -1
|
@@ -1,583 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
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
|