@loadmill/droid-cua 2.8.0 → 2.9.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.
- package/README.md +7 -4
- package/build/index.js +86 -63
- package/build/src/browser/playwright/connection.js +13 -2
- package/build/src/cli/headless-execution-config.js +4 -36
- package/build/src/cli/ink-shell.js +5 -2
- package/build/src/commands/help.js +2 -3
- package/build/src/commands/loadmill.js +10 -1
- package/build/src/commands/run.js +17 -28
- package/build/src/core/app-context.js +57 -33
- package/build/src/core/context-optimization.js +1 -1
- package/build/src/core/cua-models.js +9 -0
- package/build/src/core/execution-engine.js +2 -8
- package/build/src/core/runtime-config.js +96 -0
- package/build/src/device/loadmill.js +15 -9
- package/build/src/device/openai.js +67 -139
- package/build/src/integrations/loadmill/client.js +149 -131
- package/build/src/integrations/loadmill/executor.js +17 -11
- package/build/src/integrations/loadmill/index.js +1 -1
- package/build/src/integrations/loadmill/interpreter.js +4 -14
- package/build/src/modes/design-mode-ink.js +4 -3
- package/build/src/modes/design-mode.js +4 -3
- package/build/src/modes/execution-mode.js +26 -0
- package/build/src/prompts/execution.js +8 -106
- package/build/src/utils/console-output.js +8 -0
- package/build/src/utils/cua-debug-tracer.js +0 -1
- package/build/src/utils/structured-debug-log-manager.js +4 -1
- package/package.json +1 -1
|
@@ -9,7 +9,6 @@ import { getConfiguredStepDelayMs } from "../utils/step-delay.js";
|
|
|
9
9
|
import { printCliOutput } from "../utils/console-output.js";
|
|
10
10
|
import { annotateScreenshotArtifact, createAnnotatedScreenshotFromBase64 } from "../utils/run-screenshot-recorder.js";
|
|
11
11
|
import { CAPTURE_SOURCES } from "./capture-sources.js";
|
|
12
|
-
const VALID_CUA_MODELS = new Set(["gpt-5.4", "gpt-5.4-mini", "computer-use-preview"]);
|
|
13
12
|
function extractActionableOutputItems(items) {
|
|
14
13
|
const entries = [];
|
|
15
14
|
for (const item of items) {
|
|
@@ -189,13 +188,14 @@ export class ExecutionEngine {
|
|
|
189
188
|
this.screenshotDir = options.screenshotDir || null;
|
|
190
189
|
this.screenshotRecorder = options.screenshotRecorder || null;
|
|
191
190
|
this.strictMode = options.strictMode === true;
|
|
192
|
-
this.stepDelayMs = getConfiguredStepDelayMs();
|
|
191
|
+
this.stepDelayMs = options.stepDelayMs ?? getConfiguredStepDelayMs();
|
|
193
192
|
this.reportedScreenshotWriteError = false;
|
|
194
193
|
this.reportedScreenshotAnnotateError = false;
|
|
195
194
|
this.lastRecordedScreenshotPaths = [];
|
|
196
195
|
this.getScreenshotAsBase64 = options.getScreenshotAsBase64 || getScreenshotAsBase64;
|
|
197
196
|
this.handleModelAction = options.handleModelAction || handleModelAction;
|
|
198
197
|
this.sendCUARequest = options.sendCUARequest || sendCUARequest;
|
|
198
|
+
this.reviseTestScript = options.reviseTestScript || null;
|
|
199
199
|
this.getCurrentPlatform = options.getCurrentPlatform || getCurrentPlatform;
|
|
200
200
|
}
|
|
201
201
|
async saveScreenshotArtifact(screenshotBase64, metadata = {}, options = {}) {
|
|
@@ -579,9 +579,6 @@ export class ExecutionEngine {
|
|
|
579
579
|
note: runtimeNote
|
|
580
580
|
});
|
|
581
581
|
}
|
|
582
|
-
const selectedCuaModel = VALID_CUA_MODELS.has(process.env.OPENAI_CUA_MODEL)
|
|
583
|
-
? process.env.OPENAI_CUA_MODEL
|
|
584
|
-
: "gpt-5.4";
|
|
585
582
|
const input = [{
|
|
586
583
|
type: "computer_call_output",
|
|
587
584
|
call_id,
|
|
@@ -589,9 +586,6 @@ export class ExecutionEngine {
|
|
|
589
586
|
type: "computer_screenshot",
|
|
590
587
|
image_url: `data:image/png;base64,${screenshotBase64}`,
|
|
591
588
|
},
|
|
592
|
-
...(selectedCuaModel === "computer-use-preview"
|
|
593
|
-
? { current_url: this.getCurrentPlatform() === "ios" ? "ios://simulator" : "android://device" }
|
|
594
|
-
: {}),
|
|
595
589
|
...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
|
|
596
590
|
}];
|
|
597
591
|
if (runtimeNote) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { readContextOptimizationEnv, normalizeContextOptimizationEnabled, normalizeContextOptimizationThreshold, } from "./context-optimization.js";
|
|
3
|
+
import { resolveCuaModel } from "./cua-models.js";
|
|
4
|
+
import { normalizeStepDelayMs } from "../utils/step-delay.js";
|
|
5
|
+
export function createEmptyPromptCustomizations() {
|
|
6
|
+
return {
|
|
7
|
+
basePromptInstructions: "",
|
|
8
|
+
designModeInstructions: "",
|
|
9
|
+
executionModeInstructions: "",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function resolvePromptCustomizations(rawValue = null) {
|
|
13
|
+
const resolved = createEmptyPromptCustomizations();
|
|
14
|
+
if (!rawValue || typeof rawValue !== "object" || Array.isArray(rawValue)) {
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
for (const key of Object.keys(resolved)) {
|
|
18
|
+
if (typeof rawValue[key] === "string") {
|
|
19
|
+
resolved[key] = rawValue[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
function trimNullableString(value) {
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed || null;
|
|
30
|
+
}
|
|
31
|
+
export function resolveRuntimeCredentials({ openAiApiKey = null, loadmillApiToken = null, loadmillBaseUrl = null, } = {}) {
|
|
32
|
+
return {
|
|
33
|
+
openAiApiKey: trimNullableString(openAiApiKey),
|
|
34
|
+
loadmillApiToken: trimNullableString(loadmillApiToken),
|
|
35
|
+
loadmillBaseUrl: trimNullableString(loadmillBaseUrl),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function resolveRuntimeConfig({ cuaModel = null, strictMode = false, stepDelayMs = undefined, contextOptimizationEnabled = undefined, contextOptimizationThreshold = undefined, promptCustomizations = null, appContext = null, } = {}) {
|
|
39
|
+
return {
|
|
40
|
+
cuaModel: resolveCuaModel(cuaModel),
|
|
41
|
+
strictMode: strictMode === true,
|
|
42
|
+
stepDelayMs: normalizeStepDelayMs(Number(stepDelayMs)),
|
|
43
|
+
contextOptimizationEnabled: normalizeContextOptimizationEnabled(contextOptimizationEnabled),
|
|
44
|
+
contextOptimizationThreshold: normalizeContextOptimizationThreshold(contextOptimizationThreshold),
|
|
45
|
+
promptCustomizations: resolvePromptCustomizations(promptCustomizations),
|
|
46
|
+
appContext: appContext && typeof appContext === "object"
|
|
47
|
+
? {
|
|
48
|
+
enabled: appContext.enabled !== false,
|
|
49
|
+
path: trimNullableString(appContext.path ?? appContext.contextPath),
|
|
50
|
+
status: typeof appContext.status === "string" ? appContext.status : null,
|
|
51
|
+
text: typeof appContext.text === "string" ? appContext.text : "",
|
|
52
|
+
message: trimNullableString(appContext.message),
|
|
53
|
+
}
|
|
54
|
+
: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
path: null,
|
|
57
|
+
status: null,
|
|
58
|
+
text: "",
|
|
59
|
+
message: null,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function resolveRuntimeConfigFromEnv(env = process.env, overrides = {}) {
|
|
64
|
+
const contextOptimization = readContextOptimizationEnv(env);
|
|
65
|
+
return resolveRuntimeConfig({
|
|
66
|
+
cuaModel: env.OPENAI_CUA_MODEL,
|
|
67
|
+
stepDelayMs: env.DROID_CUA_STEP_DELAY_MS,
|
|
68
|
+
contextOptimizationEnabled: contextOptimization.enabled,
|
|
69
|
+
contextOptimizationThreshold: contextOptimization.threshold,
|
|
70
|
+
...overrides,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function resolveRuntimeCredentialsFromEnv(env = process.env, overrides = {}) {
|
|
74
|
+
return resolveRuntimeCredentials({
|
|
75
|
+
openAiApiKey: env.OPENAI_API_KEY,
|
|
76
|
+
loadmillApiToken: env.LOADMILL_API_TOKEN,
|
|
77
|
+
loadmillBaseUrl: env.LOADMILL_BASE_URL,
|
|
78
|
+
...overrides,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function createOpenAiClient(apiKey) {
|
|
82
|
+
return new OpenAI({
|
|
83
|
+
apiKey: trimNullableString(apiKey) || undefined,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function createRunContext({ config, credentials }) {
|
|
87
|
+
const resolvedConfig = resolveRuntimeConfig(config);
|
|
88
|
+
const resolvedCredentials = resolveRuntimeCredentials(credentials);
|
|
89
|
+
return {
|
|
90
|
+
config: resolvedConfig,
|
|
91
|
+
credentials: resolvedCredentials,
|
|
92
|
+
clients: {
|
|
93
|
+
openai: createOpenAiClient(resolvedCredentials.openAiApiKey),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { executeLoadmillCommand } from "../integrations/loadmill/index.js";
|
|
5
5
|
import { printCliOutput } from "../utils/console-output.js";
|
|
6
|
-
function getLoadmillSiteBaseUrl() {
|
|
7
|
-
const rawBaseUrl =
|
|
6
|
+
function getLoadmillSiteBaseUrl(loadmillClient = null) {
|
|
7
|
+
const rawBaseUrl = loadmillClient?.getBaseUrl?.() || "https://app.loadmill.com/api";
|
|
8
8
|
return rawBaseUrl.replace(/\/api\/?$/, "");
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
@@ -45,6 +45,8 @@ export function extractLoadmillCommand(userInput) {
|
|
|
45
45
|
*/
|
|
46
46
|
export async function executeLoadmillInstruction(command, isHeadlessMode, context, stepContext = null) {
|
|
47
47
|
const addOutput = context?.addOutput || printCliOutput;
|
|
48
|
+
const loadmillClient = context?.loadmillClient || null;
|
|
49
|
+
const openAiClient = context?.openAiClient || null;
|
|
48
50
|
const meta = {
|
|
49
51
|
runId: context?.runId,
|
|
50
52
|
stepId: stepContext?.stepId,
|
|
@@ -54,14 +56,16 @@ export async function executeLoadmillInstruction(command, isHeadlessMode, contex
|
|
|
54
56
|
type: 'info',
|
|
55
57
|
text: `[Loadmill] Executing: ${command}`,
|
|
56
58
|
eventType: 'system_message',
|
|
57
|
-
payload: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() },
|
|
59
|
+
payload: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) },
|
|
58
60
|
...meta
|
|
59
61
|
});
|
|
60
62
|
const result = await executeLoadmillCommand(command, {
|
|
63
|
+
loadmillClient,
|
|
64
|
+
openAiClient,
|
|
61
65
|
onProgress: ({ message, runId }) => {
|
|
62
66
|
const payload = runId
|
|
63
|
-
? { loadmillSuiteRunId: runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() }
|
|
64
|
-
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() };
|
|
67
|
+
? { loadmillSuiteRunId: runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) }
|
|
68
|
+
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) };
|
|
65
69
|
addOutput({ type: 'info', text: `[Loadmill] ${message}`, eventType: 'system_message', payload, ...meta });
|
|
66
70
|
}
|
|
67
71
|
});
|
|
@@ -84,14 +88,15 @@ export async function executeLoadmillInstruction(command, isHeadlessMode, contex
|
|
|
84
88
|
*/
|
|
85
89
|
export async function handleLoadmillFailure(command, error, isHeadlessMode, context, stepContext = null, suiteRunId = null) {
|
|
86
90
|
const addOutput = context?.addOutput || printCliOutput;
|
|
91
|
+
const loadmillClient = context?.loadmillClient || null;
|
|
87
92
|
const meta = {
|
|
88
93
|
runId: context?.runId,
|
|
89
94
|
stepId: stepContext?.stepId,
|
|
90
95
|
instructionIndex: stepContext?.instructionIndex
|
|
91
96
|
};
|
|
92
97
|
const payload = suiteRunId
|
|
93
|
-
? { loadmillSuiteRunId: suiteRunId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() }
|
|
94
|
-
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() };
|
|
98
|
+
? { loadmillSuiteRunId: suiteRunId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) }
|
|
99
|
+
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) };
|
|
95
100
|
addOutput({ type: 'error', text: '[Loadmill] FAILED', eventType: 'error', payload, ...meta });
|
|
96
101
|
addOutput({ type: 'error', text: `Command: ${command}`, eventType: 'error', payload, ...meta });
|
|
97
102
|
addOutput({ type: 'error', text: `Error: ${error}`, eventType: 'error', payload, ...meta });
|
|
@@ -148,14 +153,15 @@ export async function handleLoadmillFailure(command, error, isHeadlessMode, cont
|
|
|
148
153
|
*/
|
|
149
154
|
export function handleLoadmillSuccess(command, result, context, stepContext = null) {
|
|
150
155
|
const addOutput = context?.addOutput || printCliOutput;
|
|
156
|
+
const loadmillClient = context?.loadmillClient || null;
|
|
151
157
|
const meta = {
|
|
152
158
|
runId: context?.runId,
|
|
153
159
|
stepId: stepContext?.stepId,
|
|
154
160
|
instructionIndex: stepContext?.instructionIndex
|
|
155
161
|
};
|
|
156
162
|
const payload = result.runId
|
|
157
|
-
? { loadmillSuiteRunId: result.runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() }
|
|
158
|
-
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl() };
|
|
163
|
+
? { loadmillSuiteRunId: result.runId, loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) }
|
|
164
|
+
: { loadmillCommand: command, loadmillBaseUrl: getLoadmillSiteBaseUrl(loadmillClient) };
|
|
159
165
|
if (result.action === "search") {
|
|
160
166
|
addOutput({ type: 'success', text: `[Loadmill] Found ${result.result.flows.length} flow(s)`, eventType: 'system_message', payload, ...meta });
|
|
161
167
|
result.result.flows.forEach((flow, i) => {
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import OpenAI from "openai";
|
|
2
|
-
import { readContextOptimizationEnv } from "../core/context-optimization.js";
|
|
3
1
|
import { buildTestRevisionSystemPrompt } from "../prompts/editor.js";
|
|
4
|
-
import {
|
|
2
|
+
import { createOpenAiClient, resolveRuntimeConfig } from "../core/runtime-config.js";
|
|
3
|
+
import { DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD } from "../core/context-optimization.js";
|
|
5
4
|
import { logger } from "../utils/logger.js";
|
|
6
5
|
import { CuaDebugTracer } from "../utils/cua-debug-tracer.js";
|
|
7
|
-
let openai = null;
|
|
8
6
|
const cuaDebugTracer = new CuaDebugTracer(logger);
|
|
9
|
-
const VALID_CUA_MODELS = new Set(["gpt-5.4", "gpt-5.4-mini", "computer-use-preview"]);
|
|
10
|
-
function getSelectedCuaModel() {
|
|
11
|
-
return VALID_CUA_MODELS.has(process.env.OPENAI_CUA_MODEL) ? process.env.OPENAI_CUA_MODEL : "gpt-5.4";
|
|
12
|
-
}
|
|
13
7
|
function normalizeMessageContent(content) {
|
|
14
8
|
if (Array.isArray(content)) {
|
|
15
9
|
return content.map((item) => {
|
|
@@ -90,37 +84,14 @@ export function buildCuaInput({ messages, screenshotBase64, previousResponseId,
|
|
|
90
84
|
}
|
|
91
85
|
return input;
|
|
92
86
|
}
|
|
93
|
-
function buildContextManagement(
|
|
94
|
-
if (
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
const { enabled, threshold } = readContextOptimizationEnv();
|
|
98
|
-
if (!enabled) {
|
|
87
|
+
function buildContextManagement(contextOptimizationEnabled, contextOptimizationThreshold) {
|
|
88
|
+
if (!contextOptimizationEnabled) {
|
|
99
89
|
return undefined;
|
|
100
90
|
}
|
|
101
|
-
return [{ type: "compaction", compact_threshold:
|
|
91
|
+
return [{ type: "compaction", compact_threshold: contextOptimizationThreshold }];
|
|
102
92
|
}
|
|
103
|
-
export function buildCuaRequestParams({ cuaModel, previousResponseId,
|
|
104
|
-
const contextManagement = buildContextManagement(
|
|
105
|
-
if (cuaModel === "computer-use-preview") {
|
|
106
|
-
return {
|
|
107
|
-
model: cuaModel,
|
|
108
|
-
previous_response_id: previousResponseId || undefined,
|
|
109
|
-
input,
|
|
110
|
-
store: true,
|
|
111
|
-
truncation: "auto",
|
|
112
|
-
tools: [
|
|
113
|
-
{
|
|
114
|
-
type: "computer_use_preview",
|
|
115
|
-
display_width: deviceInfo.scaled_width,
|
|
116
|
-
display_height: deviceInfo.scaled_height,
|
|
117
|
-
environment: "browser",
|
|
118
|
-
},
|
|
119
|
-
previewClickTool,
|
|
120
|
-
],
|
|
121
|
-
reasoning: { summary: "concise" },
|
|
122
|
-
};
|
|
123
|
-
}
|
|
93
|
+
export function buildCuaRequestParams({ cuaModel, previousResponseId, input, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, }) {
|
|
94
|
+
const contextManagement = buildContextManagement(contextOptimizationEnabled, contextOptimizationThreshold);
|
|
124
95
|
return {
|
|
125
96
|
model: cuaModel,
|
|
126
97
|
reasoning: { effort: "medium" },
|
|
@@ -130,97 +101,23 @@ export function buildCuaRequestParams({ cuaModel, previousResponseId, deviceInfo
|
|
|
130
101
|
store: true,
|
|
131
102
|
truncation: "auto",
|
|
132
103
|
tools: [
|
|
133
|
-
{
|
|
134
|
-
type: "computer",
|
|
135
|
-
},
|
|
104
|
+
{ type: "computer" },
|
|
136
105
|
previewClickTool,
|
|
137
106
|
],
|
|
138
107
|
};
|
|
139
108
|
}
|
|
140
|
-
function shouldMapPreviewAccessError(err) {
|
|
141
|
-
const status = err?.status;
|
|
142
|
-
const code = typeof err?.code === "string" ? err.code.toLowerCase() : "";
|
|
143
|
-
const type = typeof err?.type === "string" ? err.type.toLowerCase() : "";
|
|
144
|
-
const message = typeof err?.message === "string" ? err.message.toLowerCase() : "";
|
|
145
|
-
if (status === 403 || status === 404)
|
|
146
|
-
return true;
|
|
147
|
-
if (code.includes("model_not_found") || code.includes("permission"))
|
|
148
|
-
return true;
|
|
149
|
-
if (type.includes("permission"))
|
|
150
|
-
return true;
|
|
151
|
-
return (message.includes("computer-use-preview") &&
|
|
152
|
-
(message.includes("access") || message.includes("permission") || message.includes("not found") || message.includes("unsupported")));
|
|
153
|
-
}
|
|
154
|
-
export function shouldMapMiniCompatibilityError(err) {
|
|
155
|
-
const status = Number(err?.status);
|
|
156
|
-
const code = typeof err?.code === "string" ? err.code.toLowerCase() : "";
|
|
157
|
-
const type = typeof err?.type === "string" ? err.type.toLowerCase() : "";
|
|
158
|
-
const message = typeof err?.message === "string" ? err.message.toLowerCase() : "";
|
|
159
|
-
const mentionsMiniModel = message.includes("gpt-5.4-mini");
|
|
160
|
-
const mentionsComputerTool = message.includes("computer") || message.includes("tool");
|
|
161
|
-
const mentionsUnsupported = (message.includes("unsupported") ||
|
|
162
|
-
message.includes("not supported") ||
|
|
163
|
-
message.includes("does not support"));
|
|
164
|
-
if (code.includes("model_not_found")) {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
if (status === 404) {
|
|
168
|
-
return mentionsMiniModel && (message.includes("not found") || mentionsUnsupported);
|
|
169
|
-
}
|
|
170
|
-
if (code.includes("unsupported")) {
|
|
171
|
-
return mentionsMiniModel || mentionsComputerTool;
|
|
172
|
-
}
|
|
173
|
-
if (!type.includes("invalid")) {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
return mentionsUnsupported && (mentionsMiniModel || mentionsComputerTool);
|
|
177
|
-
}
|
|
178
|
-
function mapCuaError(err, cuaModel) {
|
|
179
|
-
if (cuaModel === "computer-use-preview" && shouldMapPreviewAccessError(err)) {
|
|
180
|
-
const mapped = new Error("OpenAI API key does not have access to computer-use-preview. Switch to gpt-5.4 in Settings > CUA Model.");
|
|
181
|
-
mapped.cause = err;
|
|
182
|
-
return mapped;
|
|
183
|
-
}
|
|
184
|
-
if (cuaModel === "gpt-5.4-mini" && shouldMapMiniCompatibilityError(err)) {
|
|
185
|
-
const mapped = new Error("OpenAI rejected gpt-5.4-mini for computer-use execution. Switch to gpt-5.4 or computer-use-preview in Settings > CUA Model.");
|
|
186
|
-
mapped.cause = err;
|
|
187
|
-
return mapped;
|
|
188
|
-
}
|
|
189
|
-
return err;
|
|
190
|
-
}
|
|
191
|
-
export function isNonRetryableCuaError(err) {
|
|
192
|
-
const status = Number(err?.status);
|
|
193
|
-
const code = typeof err?.code === "string" ? err.code.toLowerCase() : "";
|
|
194
|
-
const type = typeof err?.type === "string" ? err.type.toLowerCase() : "";
|
|
195
|
-
const message = typeof err?.message === "string" ? err.message.toLowerCase() : "";
|
|
196
|
-
if ([401, 403, 404].includes(status)) {
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
return (code.includes("model_not_found") ||
|
|
200
|
-
code.includes("permission") ||
|
|
201
|
-
type.includes("permission") ||
|
|
202
|
-
message.includes("does not have access to computer-use-preview") ||
|
|
203
|
-
message.includes("switch to gpt-5.4 in settings > cua model") ||
|
|
204
|
-
message.includes("openai rejected gpt-5.4-mini for computer-use execution") ||
|
|
205
|
-
message.includes("switch to gpt-5.4 or computer-use-preview in settings > cua model"));
|
|
206
|
-
}
|
|
207
|
-
function getOpenAI() {
|
|
208
|
-
if (!openai) {
|
|
209
|
-
openai = new OpenAI({
|
|
210
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
return openai;
|
|
214
|
-
}
|
|
215
109
|
/**
|
|
216
110
|
* Revise a test script based on user feedback using simple chat completion
|
|
217
|
-
* @param {
|
|
218
|
-
*
|
|
111
|
+
* @param {{
|
|
112
|
+
* client: import("openai").default,
|
|
113
|
+
* originalScript: string,
|
|
114
|
+
* revisionRequest: string,
|
|
115
|
+
* }} options
|
|
219
116
|
* @returns {Promise<string>} - The revised test script
|
|
220
117
|
*/
|
|
221
|
-
export async function reviseTestScript(originalScript, revisionRequest) {
|
|
222
|
-
const response = await
|
|
223
|
-
model: "gpt-
|
|
118
|
+
export async function reviseTestScript({ client, originalScript, revisionRequest }) {
|
|
119
|
+
const response = await client.chat.completions.create({
|
|
120
|
+
model: "gpt-5.4",
|
|
224
121
|
messages: [{
|
|
225
122
|
role: "system",
|
|
226
123
|
content: buildTestRevisionSystemPrompt(originalScript, revisionRequest)
|
|
@@ -228,29 +125,28 @@ export async function reviseTestScript(originalScript, revisionRequest) {
|
|
|
228
125
|
});
|
|
229
126
|
return response.choices[0].message.content.trim();
|
|
230
127
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
export async function sendCUARequest({ messages, screenshotBase64, previousResponseId, callId, deviceInfo, debugContext, }) {
|
|
247
|
-
const cuaModel = getSelectedCuaModel();
|
|
128
|
+
/**
|
|
129
|
+
* @param {{
|
|
130
|
+
* client: import("openai").default,
|
|
131
|
+
* cuaModel: string,
|
|
132
|
+
* contextOptimizationEnabled?: boolean,
|
|
133
|
+
* contextOptimizationThreshold?: number,
|
|
134
|
+
* messages: Array<unknown>,
|
|
135
|
+
* screenshotBase64?: string,
|
|
136
|
+
* previousResponseId?: string | null,
|
|
137
|
+
* callId?: string | null,
|
|
138
|
+
* deviceInfo?: unknown,
|
|
139
|
+
* debugContext?: unknown,
|
|
140
|
+
* }} options
|
|
141
|
+
*/
|
|
142
|
+
export async function sendCUARequest({ client, cuaModel, contextOptimizationEnabled = true, contextOptimizationThreshold = DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, messages, screenshotBase64, previousResponseId, callId, deviceInfo, debugContext, }) {
|
|
248
143
|
const input = buildCuaInput({ messages, screenshotBase64, previousResponseId, callId });
|
|
249
144
|
const requestParams = buildCuaRequestParams({
|
|
250
145
|
cuaModel,
|
|
251
146
|
previousResponseId,
|
|
252
|
-
deviceInfo,
|
|
253
147
|
input,
|
|
148
|
+
contextOptimizationEnabled,
|
|
149
|
+
contextOptimizationThreshold,
|
|
254
150
|
});
|
|
255
151
|
const trace = cuaDebugTracer.startTurn({
|
|
256
152
|
requestParams,
|
|
@@ -262,12 +158,44 @@ export async function sendCUARequest({ messages, screenshotBase64, previousRespo
|
|
|
262
158
|
});
|
|
263
159
|
logger.debug("CUA Request:", trace.requestLog);
|
|
264
160
|
try {
|
|
265
|
-
const response = await
|
|
161
|
+
const response = await client.responses.create(requestParams);
|
|
266
162
|
cuaDebugTracer.onResponse(trace, response);
|
|
267
163
|
return response;
|
|
268
164
|
}
|
|
269
165
|
catch (err) {
|
|
270
166
|
cuaDebugTracer.onError(trace, err);
|
|
271
|
-
throw
|
|
167
|
+
throw err;
|
|
272
168
|
}
|
|
273
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* @param {{
|
|
172
|
+
* client?: import("openai").default | null,
|
|
173
|
+
* config?: Record<string, unknown>,
|
|
174
|
+
* }} options
|
|
175
|
+
*/
|
|
176
|
+
export function createOpenAiExecutor({ client, config }) {
|
|
177
|
+
const runtimeConfig = resolveRuntimeConfig(config);
|
|
178
|
+
const openAiClient = client || createOpenAiClient(null);
|
|
179
|
+
return {
|
|
180
|
+
client: openAiClient,
|
|
181
|
+
config: runtimeConfig,
|
|
182
|
+
async sendCUARequest(args) {
|
|
183
|
+
// Bind model and request-shaping config to the active run so execution
|
|
184
|
+
// stays deterministic even if settings change after the run starts.
|
|
185
|
+
return await sendCUARequest({
|
|
186
|
+
client: openAiClient,
|
|
187
|
+
cuaModel: runtimeConfig.cuaModel,
|
|
188
|
+
contextOptimizationEnabled: runtimeConfig.contextOptimizationEnabled,
|
|
189
|
+
contextOptimizationThreshold: runtimeConfig.contextOptimizationThreshold,
|
|
190
|
+
...args,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async reviseTestScript(originalScript, revisionRequest) {
|
|
194
|
+
return await reviseTestScript({
|
|
195
|
+
client: openAiClient,
|
|
196
|
+
originalScript,
|
|
197
|
+
revisionRequest,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|