@steipete/oracle 0.15.0 → 0.15.2
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/bin/oracle-cli.js +14 -6
- package/dist/docs-site/bridge.html +17 -1
- package/dist/docs-site/browser-mode.html +2 -2
- package/dist/docs-site/configuration.html +12 -2
- package/dist/docs-site/openai-endpoints.html +12 -0
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +81 -50
- package/dist/src/browser/actions/attachments.js +31 -5
- package/dist/src/browser/actions/deepResearch.js +218 -73
- package/dist/src/browser/actions/modelSelection.js +30 -7
- package/dist/src/browser/actions/promptComposer.js +75 -19
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/artifacts.js +191 -6
- package/dist/src/browser/chatgptFiles.js +529 -98
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +1 -0
- package/dist/src/browser/constants.js +6 -0
- package/dist/src/browser/conversationTurns.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +64 -0
- package/dist/src/browser/cookies.js +72 -0
- package/dist/src/browser/index.js +103 -94
- package/dist/src/browser/projectSourcesRunner.js +3 -2
- package/dist/src/browser/reattach.js +27 -11
- package/dist/src/browser/reattachHelpers.js +14 -5
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +28 -10
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/oscProgress.js +3 -2
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +20 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -5,8 +5,9 @@ import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
|
5
5
|
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
|
|
6
6
|
import { launchChrome, connectToChrome, hideChromeWindow, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
|
|
7
7
|
import { resolveBrowserConfig } from "./config.js";
|
|
8
|
-
import { syncCookies } from "./cookies.js";
|
|
9
|
-
import { CHATGPT_URL
|
|
8
|
+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
9
|
+
import { CHATGPT_URL } from "./constants.js";
|
|
10
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
10
11
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
11
12
|
import { readDevToolsActivePortInfo } from "./detect.js";
|
|
12
13
|
import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
|
|
@@ -14,6 +15,12 @@ import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
|
|
|
14
15
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
15
16
|
const recoverSession = deps.recoverSession ??
|
|
16
17
|
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
18
|
+
let closeAttachedConnection = null;
|
|
19
|
+
const closeAttached = async () => {
|
|
20
|
+
const close = closeAttachedConnection;
|
|
21
|
+
closeAttachedConnection = null;
|
|
22
|
+
await close?.().catch(() => undefined);
|
|
23
|
+
};
|
|
17
24
|
if (!runtime.chromePort && !runtime.chromeBrowserWSEndpoint) {
|
|
18
25
|
logger("No running Chrome detected; reopening browser to locate the session.");
|
|
19
26
|
return recoverSession(runtime, config);
|
|
@@ -37,8 +44,8 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
37
44
|
targetId: target?.targetId ?? target?.id,
|
|
38
45
|
closeTargetOnDispose: false,
|
|
39
46
|
})
|
|
40
|
-
: {
|
|
41
|
-
client
|
|
47
|
+
: await (async () => {
|
|
48
|
+
const client = (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
|
|
42
49
|
? {
|
|
43
50
|
target: browserWSEndpoint,
|
|
44
51
|
local: true,
|
|
@@ -48,9 +55,10 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
48
55
|
host,
|
|
49
56
|
port,
|
|
50
57
|
target: target?.targetId ?? target?.id,
|
|
51
|
-
}))
|
|
52
|
-
close:
|
|
53
|
-
};
|
|
58
|
+
}));
|
|
59
|
+
return { client, close: () => client.close() };
|
|
60
|
+
})();
|
|
61
|
+
closeAttachedConnection = () => connection.close();
|
|
54
62
|
const client = connection.client;
|
|
55
63
|
const { Runtime, DOM, Page } = client;
|
|
56
64
|
if (Runtime?.enable) {
|
|
@@ -97,7 +105,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
97
105
|
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
98
106
|
requireScopedTargetOwner: true,
|
|
99
107
|
}), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
100
|
-
await
|
|
108
|
+
await closeAttached();
|
|
101
109
|
return {
|
|
102
110
|
answerText: researchResult.text,
|
|
103
111
|
answerMarkdown: researchResult.text,
|
|
@@ -108,10 +116,11 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
108
116
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
109
117
|
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
110
118
|
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
111
|
-
await
|
|
119
|
+
await closeAttached();
|
|
112
120
|
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
113
121
|
}
|
|
114
122
|
catch (error) {
|
|
123
|
+
await closeAttached();
|
|
115
124
|
const message = error instanceof Error ? error.message : String(error);
|
|
116
125
|
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
117
126
|
return recoverSession(runtime, config);
|
|
@@ -163,7 +172,7 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
163
172
|
const chrome = await launchChrome(resolved, userDataDir, logger);
|
|
164
173
|
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
165
174
|
const client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
166
|
-
const { Network, Page, Runtime, DOM } = client;
|
|
175
|
+
const { Network, Page, Runtime, DOM, Target } = client;
|
|
167
176
|
if (Runtime?.enable) {
|
|
168
177
|
await Runtime.enable();
|
|
169
178
|
}
|
|
@@ -183,6 +192,13 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
183
192
|
waitMs: resolved.cookieSyncWaitMs ?? 0,
|
|
184
193
|
});
|
|
185
194
|
}
|
|
195
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
196
|
+
preserveConversationIds: [
|
|
197
|
+
runtime.conversationId,
|
|
198
|
+
extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
199
|
+
extractConversationIdFromUrl(resolved.url),
|
|
200
|
+
],
|
|
201
|
+
});
|
|
186
202
|
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
187
203
|
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
188
204
|
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
@@ -268,7 +284,7 @@ async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
|
|
|
268
284
|
const needle = ${JSON.stringify(preview.toLowerCase().replace(/\s+/g, " ").slice(0, 120))};
|
|
269
285
|
if (!needle) return null;
|
|
270
286
|
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
271
|
-
const turns =
|
|
287
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
272
288
|
let matched = null;
|
|
273
289
|
for (const [index, node] of turns.entries()) {
|
|
274
290
|
const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
2
3
|
import { delay } from "./utils.js";
|
|
3
4
|
import { readAssistantSnapshot } from "./pageActions.js";
|
|
4
5
|
export function pickTarget(targets, runtime) {
|
|
5
6
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
6
7
|
return undefined;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const conversationId = runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? "");
|
|
10
|
+
const byId = runtime.chromeTargetId
|
|
11
|
+
? targets.find((target) => (target.targetId ?? target.id) === runtime.chromeTargetId)
|
|
12
|
+
: undefined;
|
|
13
|
+
if (conversationId) {
|
|
14
|
+
if (byId && extractConversationIdFromUrl(byId.url ?? "") === conversationId) {
|
|
11
15
|
return byId;
|
|
16
|
+
}
|
|
17
|
+
const byConversation = targets.find((target) => extractConversationIdFromUrl(target.url ?? "") === conversationId);
|
|
18
|
+
if (byConversation)
|
|
19
|
+
return byConversation;
|
|
12
20
|
}
|
|
21
|
+
if (byId)
|
|
22
|
+
return byId;
|
|
13
23
|
if (runtime.tabUrl) {
|
|
14
24
|
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
15
25
|
targets.find((t) => runtime.tabUrl.startsWith(t.url || ""));
|
|
@@ -253,10 +263,9 @@ export async function waitForLocationChange(Runtime, timeoutMs) {
|
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
export async function readConversationTurnIndex(Runtime, logger) {
|
|
256
|
-
const selectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
257
266
|
try {
|
|
258
267
|
const { result } = await Runtime.evaluate({
|
|
259
|
-
expression:
|
|
268
|
+
expression: buildConversationTurnCountExpression(),
|
|
260
269
|
returnByValue: true,
|
|
261
270
|
});
|
|
262
271
|
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
@@ -127,11 +127,17 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
127
127
|
generateImagePath: runOptions.generateImage,
|
|
128
128
|
outputPath: runOptions.outputPath,
|
|
129
129
|
followUpPrompts: runOptions.browserFollowUps,
|
|
130
|
-
runtimeHintCb: async (runtime) => {
|
|
131
|
-
|
|
130
|
+
runtimeHintCb: async (runtime, modelSelection) => {
|
|
131
|
+
const runtimeWithController = {
|
|
132
132
|
...runtime,
|
|
133
133
|
controllerPid: runtime.controllerPid ?? process.pid,
|
|
134
|
-
}
|
|
134
|
+
};
|
|
135
|
+
if (modelSelection) {
|
|
136
|
+
await persistRuntimeHint(runtimeWithController, modelSelection);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await persistRuntimeHint(runtimeWithController);
|
|
140
|
+
}
|
|
135
141
|
},
|
|
136
142
|
});
|
|
137
143
|
}
|
|
@@ -21,7 +21,10 @@ export async function runBridgeClient(options) {
|
|
|
21
21
|
const suffix = health.statusCode ? ` (HTTP ${health.statusCode})` : "";
|
|
22
22
|
throw new Error(`Remote service health check failed: ${health.error ?? "unknown error"}${suffix}`);
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
const artifactTransfer = health.capabilities?.artifactTransfer
|
|
25
|
+
? ` — artifacts bridge v${health.capabilities.artifactProtocolVersion}`
|
|
26
|
+
: " — artifact transfer unavailable; file downloads require manual copy";
|
|
27
|
+
console.log(chalk.green(`Remote service OK (${remoteHost})${health.version ? ` — oracle ${health.version}` : ""}${artifactTransfer}`));
|
|
25
28
|
}
|
|
26
29
|
const configFilePath = options.config?.trim() || defaultConfigPath();
|
|
27
30
|
if (options.writeConfig !== false) {
|
|
@@ -60,6 +60,13 @@ export async function runBridgeDoctor(_options) {
|
|
|
60
60
|
if (health.ok) {
|
|
61
61
|
const meta = health.version ? `oracle ${health.version}` : "ok";
|
|
62
62
|
lines.push(chalk.dim(`Auth (/health): ${chalk.green(meta)}`));
|
|
63
|
+
if (health.capabilities?.artifactTransfer) {
|
|
64
|
+
lines.push(chalk.dim(`Artifact transfer: ${chalk.green(`bridge v${health.capabilities.artifactProtocolVersion}`)} (${formatBytes(health.capabilities.maxArtifactBytes)} max)`));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
warn.push("Remote host does not advertise bridge artifact transfer; ChatGPT-generated files may need manual copy from the browser host.");
|
|
68
|
+
lines.push(chalk.dim(`Artifact transfer: ${chalk.yellow("manual fallback")}`));
|
|
69
|
+
}
|
|
63
70
|
}
|
|
64
71
|
else {
|
|
65
72
|
const detail = health.error ?? "unknown error";
|
|
@@ -118,3 +125,15 @@ export async function runBridgeDoctor(_options) {
|
|
|
118
125
|
console.log(lines.join("\n"));
|
|
119
126
|
process.exitCode = fail.length ? 1 : 0;
|
|
120
127
|
}
|
|
128
|
+
function formatBytes(bytes) {
|
|
129
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
130
|
+
return "unknown";
|
|
131
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
132
|
+
let value = bytes;
|
|
133
|
+
let unit = 0;
|
|
134
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
135
|
+
value /= 1024;
|
|
136
|
+
unit += 1;
|
|
137
|
+
}
|
|
138
|
+
return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
139
|
+
}
|
|
@@ -2,6 +2,7 @@ import { DEFAULT_MODEL, MODEL_CONFIGS } from "../oracle.js";
|
|
|
2
2
|
import { resolveEngine } from "./engine.js";
|
|
3
3
|
import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBaseUrl, } from "./options.js";
|
|
4
4
|
import { resolveGeminiModelId } from "../oracle/gemini.js";
|
|
5
|
+
import { resolveOverriddenApiModel } from "../oracle/modelResolver.js";
|
|
5
6
|
import { PromptValidationError } from "../oracle/errors.js";
|
|
6
7
|
import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
|
|
7
8
|
import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
|
|
@@ -62,7 +63,8 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
62
63
|
// Silent coerce; multi-model still forces API.
|
|
63
64
|
}
|
|
64
65
|
const chosenModel = uniqueMultiModels[0] ?? resolvedModel;
|
|
65
|
-
const
|
|
66
|
+
const apiModelOverrides = fixedEngine === "api" ? userConfig?.modelOverrides : undefined;
|
|
67
|
+
const effectiveModelId = resolveEffectiveModelId(chosenModel, apiModelOverrides);
|
|
66
68
|
const runOptions = {
|
|
67
69
|
prompt: promptWithSuffix,
|
|
68
70
|
model: chosenModel,
|
|
@@ -76,6 +78,7 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
76
78
|
baseUrl,
|
|
77
79
|
azure,
|
|
78
80
|
effectiveModelId,
|
|
81
|
+
modelOverrides: apiModelOverrides,
|
|
79
82
|
};
|
|
80
83
|
return { runOptions, resolvedEngine: fixedEngine, engineCoercedToApi };
|
|
81
84
|
}
|
|
@@ -90,7 +93,13 @@ function resolveAzureOptions(userConfig, env) {
|
|
|
90
93
|
apiVersion: env.AZURE_OPENAI_API_VERSION ?? userConfig?.azure?.apiVersion,
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
|
-
function resolveEffectiveModelId(model) {
|
|
96
|
+
function resolveEffectiveModelId(model, modelOverrides) {
|
|
97
|
+
// A user-config override of a known model's apiModel must win, since this id
|
|
98
|
+
// becomes the on-wire request model id in run.ts (including for Gemini aliases).
|
|
99
|
+
const overridden = resolveOverriddenApiModel(model, modelOverrides);
|
|
100
|
+
if (overridden) {
|
|
101
|
+
return overridden;
|
|
102
|
+
}
|
|
94
103
|
if (typeof model === "string" && model.startsWith("gemini")) {
|
|
95
104
|
return resolveGeminiModelId(model);
|
|
96
105
|
}
|
|
@@ -306,7 +306,12 @@ export async function attachSession(sessionId, options) {
|
|
|
306
306
|
for (const artifact of metadata.artifacts) {
|
|
307
307
|
const label = artifact.label ?? artifact.kind;
|
|
308
308
|
const size = artifact.sizeBytes ? ` (${formatBytes(artifact.sizeBytes)})` : "";
|
|
309
|
-
|
|
309
|
+
const checksum = artifact.sha256 ? ` sha256=${artifact.sha256.slice(0, 12)}…` : "";
|
|
310
|
+
const validation = artifact.validation
|
|
311
|
+
? ` validation=${artifact.validation.ok ? "ok" : (artifact.validation.error ?? "failed")}`
|
|
312
|
+
: "";
|
|
313
|
+
const transfer = artifact.transfer?.status ? ` transfer=${artifact.transfer.status}` : "";
|
|
314
|
+
console.log(`- ${chalk.cyan(label)} — ${artifact.path}${size}${checksum}${validation}${transfer}`);
|
|
310
315
|
}
|
|
311
316
|
}
|
|
312
317
|
const responseSummary = formatResponseMetadata(metadata.response);
|
|
@@ -33,6 +33,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
33
33
|
write(chunk);
|
|
34
34
|
return muteStdout ? true : process.stdout.write(chunk);
|
|
35
35
|
};
|
|
36
|
+
let currentBrowser = browserConfig
|
|
37
|
+
? { config: browserConfig }
|
|
38
|
+
: sessionMeta.browser;
|
|
36
39
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
37
40
|
status: "running",
|
|
38
41
|
startedAt: new Date().toISOString(),
|
|
@@ -54,11 +57,19 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
54
57
|
}
|
|
55
58
|
const runnerDeps = {
|
|
56
59
|
...browserDeps,
|
|
57
|
-
persistRuntimeHint: async (runtime) => {
|
|
60
|
+
persistRuntimeHint: async (runtime, modelSelection) => {
|
|
61
|
+
const browser = {
|
|
62
|
+
config: browserConfig,
|
|
63
|
+
runtime,
|
|
64
|
+
...(modelSelection ? { modelSelection } : {}),
|
|
65
|
+
};
|
|
58
66
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
59
67
|
status: "running",
|
|
60
|
-
browser
|
|
68
|
+
browser,
|
|
61
69
|
});
|
|
70
|
+
// Keep this attempt's copy fresh so error paths fall back to the
|
|
71
|
+
// latest persisted browser evidence instead of stale session input.
|
|
72
|
+
currentBrowser = browser;
|
|
62
73
|
},
|
|
63
74
|
};
|
|
64
75
|
const result = await runBrowserSessionExecution({
|
|
@@ -112,6 +123,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
112
123
|
const modelConfig = await resolveModelConfig(primaryModel, {
|
|
113
124
|
baseUrl: runOptions.baseUrl,
|
|
114
125
|
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
126
|
+
modelOverrides: runOptions.modelOverrides,
|
|
115
127
|
});
|
|
116
128
|
const files = await readFiles(runOptions.file ?? [], {
|
|
117
129
|
cwd,
|
|
@@ -403,7 +415,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
403
415
|
if (connectionLost && mode === "browser" && browserCanReattach) {
|
|
404
416
|
const runtime = userError.details
|
|
405
417
|
?.runtime;
|
|
406
|
-
const recoverableRuntime = runtime ??
|
|
418
|
+
const recoverableRuntime = runtime ?? currentBrowser?.runtime;
|
|
407
419
|
if (!hasRecoverableChatGptConversation(recoverableRuntime) &&
|
|
408
420
|
recoverableRuntime?.promptSubmitted !== true) {
|
|
409
421
|
log(dim("Chrome disconnected before a ChatGPT conversation was created; marking session error."));
|
|
@@ -425,6 +437,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
425
437
|
errorMessage: message,
|
|
426
438
|
mode,
|
|
427
439
|
browser: {
|
|
440
|
+
...currentBrowser,
|
|
428
441
|
config: browserConfig,
|
|
429
442
|
runtime: recoverableRuntime,
|
|
430
443
|
},
|
|
@@ -449,8 +462,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
449
462
|
errorMessage: message,
|
|
450
463
|
mode,
|
|
451
464
|
browser: {
|
|
465
|
+
...currentBrowser,
|
|
452
466
|
config: browserConfig,
|
|
453
|
-
runtime: runtime ??
|
|
467
|
+
runtime: runtime ?? currentBrowser?.runtime,
|
|
454
468
|
},
|
|
455
469
|
response: { status: "running", incompleteReason: "chrome-disconnected" },
|
|
456
470
|
});
|
|
@@ -479,8 +493,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
479
493
|
errorMessage: message,
|
|
480
494
|
mode,
|
|
481
495
|
browser: {
|
|
496
|
+
...currentBrowser,
|
|
482
497
|
config: browserConfig,
|
|
483
|
-
runtime: runtime ??
|
|
498
|
+
runtime: runtime ?? currentBrowser?.runtime,
|
|
484
499
|
},
|
|
485
500
|
response: { status: "incomplete", incompleteReason: "incomplete-capture" },
|
|
486
501
|
error: {
|
|
@@ -491,11 +506,12 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
491
506
|
});
|
|
492
507
|
const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
493
508
|
if (autoReattachIntervalMs > 0) {
|
|
494
|
-
const autoRuntime = runtime ??
|
|
509
|
+
const autoRuntime = runtime ?? currentBrowser?.runtime;
|
|
495
510
|
const success = await autoReattachUntilComplete({
|
|
496
511
|
sessionMeta,
|
|
497
512
|
runtime: autoRuntime ?? undefined,
|
|
498
513
|
browserConfig,
|
|
514
|
+
browserMetadata: currentBrowser,
|
|
499
515
|
runOptions,
|
|
500
516
|
modelForStatus,
|
|
501
517
|
notificationSettings,
|
|
@@ -505,7 +521,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
505
521
|
return;
|
|
506
522
|
}
|
|
507
523
|
}
|
|
508
|
-
logBrowserReattachGuidance(runtime ??
|
|
524
|
+
logBrowserReattachGuidance(runtime ?? currentBrowser?.runtime);
|
|
509
525
|
return;
|
|
510
526
|
}
|
|
511
527
|
if (cloudflareChallenge && mode === "browser") {
|
|
@@ -537,7 +553,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
537
553
|
? userError?.details?.runtime
|
|
538
554
|
: undefined;
|
|
539
555
|
if (!cloudflareChallenge && browserCanReattach) {
|
|
540
|
-
logBrowserReattachGuidance(browserRuntime ??
|
|
556
|
+
logBrowserReattachGuidance(browserRuntime ?? currentBrowser?.runtime);
|
|
541
557
|
}
|
|
542
558
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
543
559
|
status: "error",
|
|
@@ -546,8 +562,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
546
562
|
mode,
|
|
547
563
|
browser: browserConfig
|
|
548
564
|
? {
|
|
565
|
+
...currentBrowser,
|
|
549
566
|
config: browserConfig,
|
|
550
|
-
runtime: browserRuntime ??
|
|
567
|
+
runtime: browserRuntime ?? currentBrowser?.runtime,
|
|
551
568
|
}
|
|
552
569
|
: undefined,
|
|
553
570
|
response: responseMetadata,
|
|
@@ -816,7 +833,7 @@ async function writeAssistantOutput(targetPath, content, log) {
|
|
|
816
833
|
log(dim(`write-output failed (${reason}); session completed anyway.`));
|
|
817
834
|
}
|
|
818
835
|
}
|
|
819
|
-
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, runOptions, modelForStatus, notificationSettings, log, }) {
|
|
836
|
+
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, }) {
|
|
820
837
|
if (!runtime || !browserConfig) {
|
|
821
838
|
log(dim("Auto-reattach disabled: missing runtime or browser config."));
|
|
822
839
|
return false;
|
|
@@ -898,6 +915,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
898
915
|
},
|
|
899
916
|
errorMessage: undefined,
|
|
900
917
|
browser: {
|
|
918
|
+
...browserMetadata,
|
|
901
919
|
config: browserConfig,
|
|
902
920
|
runtime,
|
|
903
921
|
},
|
package/dist/src/config.js
CHANGED
|
@@ -125,6 +125,9 @@ function sanitizeProjectConfig(config) {
|
|
|
125
125
|
sanitized.background = config.background;
|
|
126
126
|
if (config.promptSuffix !== undefined)
|
|
127
127
|
sanitized.promptSuffix = config.promptSuffix;
|
|
128
|
+
// NOTE: `modelOverrides` is intentionally NOT copied here. Model routing
|
|
129
|
+
// overrides are user-config only; allowing them from project configs would let
|
|
130
|
+
// an untrusted repository silently redirect API calls (apiModel) or reasoning.
|
|
128
131
|
if (config.browser) {
|
|
129
132
|
sanitized.browser = {};
|
|
130
133
|
const browser = config.browser;
|
|
@@ -141,6 +141,8 @@ function buildOpenRouterCompletionClient(instance) {
|
|
|
141
141
|
model: body.model,
|
|
142
142
|
messages,
|
|
143
143
|
max_tokens: body.max_output_tokens,
|
|
144
|
+
// Custom gateways use Chat Completions, whose effort field differs from Responses.
|
|
145
|
+
...(body.reasoning?.effort ? { reasoning_effort: body.reasoning.effort } : {}),
|
|
144
146
|
};
|
|
145
147
|
const streaming = { ...base, stream: true };
|
|
146
148
|
const nonStreaming = { ...base, stream: false };
|
|
@@ -120,6 +120,12 @@ function mapToOpenRouterId(candidate, catalog, providerHint) {
|
|
|
120
120
|
return candidate;
|
|
121
121
|
}
|
|
122
122
|
export async function resolveModelConfig(model, options = {}) {
|
|
123
|
+
const base = await resolveBaseModelConfig(model, options);
|
|
124
|
+
// Apply user-config per-model overrides last, after known/OpenRouter/synthesized
|
|
125
|
+
// resolution, so an explicit override always wins.
|
|
126
|
+
return applyModelOverride(base, model, options.modelOverrides);
|
|
127
|
+
}
|
|
128
|
+
async function resolveBaseModelConfig(model, options = {}) {
|
|
123
129
|
const known = isKnownModel(model) ? MODEL_CONFIGS[model] : null;
|
|
124
130
|
const fetcher = options.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
125
131
|
const openRouterActive = isOpenRouterBaseUrl(options.baseUrl) || Boolean(options.openRouterApiKey);
|
|
@@ -186,6 +192,85 @@ export async function resolveModelConfig(model, options = {}) {
|
|
|
186
192
|
export function isProModel(model) {
|
|
187
193
|
return isKnownModel(model) && PRO_MODELS.has(model);
|
|
188
194
|
}
|
|
195
|
+
const VALID_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
196
|
+
function isRecord(value) {
|
|
197
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns the override's `apiModel` for a *known* model when present and non-empty,
|
|
201
|
+
* otherwise `undefined`. Single source of truth for the override apiModel rule,
|
|
202
|
+
* shared by {@link applyModelOverride} and the CLI's `effectiveModelId` resolution.
|
|
203
|
+
*/
|
|
204
|
+
export function resolveOverriddenApiModel(model, overrides) {
|
|
205
|
+
if (!overrides || !isKnownModel(model))
|
|
206
|
+
return undefined;
|
|
207
|
+
const override = overrides[model];
|
|
208
|
+
if (!isRecord(override))
|
|
209
|
+
return undefined;
|
|
210
|
+
if (typeof override.apiModel === "string" && override.apiModel.trim() !== "") {
|
|
211
|
+
return override.apiModel.trim();
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Apply a user-config per-model override on top of a resolved config.
|
|
217
|
+
*
|
|
218
|
+
* Scope is intentionally narrow: only *known* models can be overridden, so the
|
|
219
|
+
* tokenizer (a function, not expressible in JSON) and any unspecified fields are
|
|
220
|
+
* inherited from the base config. Override fields are validated defensively
|
|
221
|
+
* because they come from user-authored JSON5.
|
|
222
|
+
*/
|
|
223
|
+
export function applyModelOverride(base, model, overrides) {
|
|
224
|
+
if (!overrides || !isKnownModel(model))
|
|
225
|
+
return base;
|
|
226
|
+
const override = overrides[model];
|
|
227
|
+
if (!isRecord(override))
|
|
228
|
+
return base;
|
|
229
|
+
const result = { ...base };
|
|
230
|
+
const apiModel = resolveOverriddenApiModel(model, overrides);
|
|
231
|
+
if (apiModel) {
|
|
232
|
+
result.apiModel = apiModel;
|
|
233
|
+
}
|
|
234
|
+
if (Object.hasOwn(override, "reasoning")) {
|
|
235
|
+
const reasoning = override.reasoning;
|
|
236
|
+
if (reasoning === null) {
|
|
237
|
+
// Explicit null clears the known model's reasoning effort.
|
|
238
|
+
result.reasoning = null;
|
|
239
|
+
}
|
|
240
|
+
else if (isRecord(reasoning) &&
|
|
241
|
+
typeof reasoning.effort === "string" &&
|
|
242
|
+
VALID_REASONING_EFFORTS.includes(reasoning.effort)) {
|
|
243
|
+
result.reasoning = { effort: reasoning.effort };
|
|
244
|
+
}
|
|
245
|
+
// Malformed reasoning override is ignored (base value preserved).
|
|
246
|
+
}
|
|
247
|
+
if (typeof override.inputLimit === "number" &&
|
|
248
|
+
Number.isSafeInteger(override.inputLimit) &&
|
|
249
|
+
override.inputLimit > 0) {
|
|
250
|
+
result.inputLimit = override.inputLimit;
|
|
251
|
+
}
|
|
252
|
+
// Non-positive or non-integer inputLimit (e.g. 0, 0.5, NaN, Infinity) is ignored.
|
|
253
|
+
if (Object.hasOwn(override, "pricing")) {
|
|
254
|
+
const pricing = override.pricing;
|
|
255
|
+
if (pricing === null) {
|
|
256
|
+
result.pricing = null;
|
|
257
|
+
}
|
|
258
|
+
else if (isRecord(pricing) &&
|
|
259
|
+
typeof pricing.inputPerToken === "number" &&
|
|
260
|
+
Number.isFinite(pricing.inputPerToken) &&
|
|
261
|
+
pricing.inputPerToken >= 0 &&
|
|
262
|
+
typeof pricing.outputPerToken === "number" &&
|
|
263
|
+
Number.isFinite(pricing.outputPerToken) &&
|
|
264
|
+
pricing.outputPerToken >= 0) {
|
|
265
|
+
result.pricing = {
|
|
266
|
+
inputPerToken: pricing.inputPerToken,
|
|
267
|
+
outputPerToken: pricing.outputPerToken,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// Malformed pricing override is ignored (base value preserved).
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
189
274
|
export function resetOpenRouterCatalogCacheForTest() {
|
|
190
275
|
catalogCache.clear();
|
|
191
276
|
}
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
|
|
4
4
|
import { sessionStore } from "../sessionStore.js";
|
|
5
|
+
import { resolveOverriddenApiModel } from "./modelResolver.js";
|
|
5
6
|
import { findOscProgressSequences, OSC_PROGRESS_PREFIX } from "osc-progress";
|
|
6
7
|
function forwardOscProgress(chunk, shouldForward) {
|
|
7
8
|
if (!shouldForward || !chunk.includes(OSC_PROGRESS_PREFIX)) {
|
|
@@ -81,7 +82,9 @@ function startModelExecution({ sessionMeta, runOptions, model, cwd, store, runOr
|
|
|
81
82
|
});
|
|
82
83
|
const result = await runOracleImpl({
|
|
83
84
|
...perModelOptions,
|
|
84
|
-
|
|
85
|
+
// Respect a user-config apiModel override for this known model; falls back
|
|
86
|
+
// to the canonical model id (unchanged behavior) when no override applies.
|
|
87
|
+
effectiveModelId: resolveOverriddenApiModel(model, runOptions.modelOverrides) ?? model,
|
|
85
88
|
// Drop per-model preamble; the aggregate runner prints the shared header and tips once.
|
|
86
89
|
suppressHeader: true,
|
|
87
90
|
suppressAnswerHeader: true,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { startOscProgress as startOscProgressShared, supportsOscProgress as supportsOscProgressShared, } from "osc-progress";
|
|
3
|
-
export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY) {
|
|
3
|
+
export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY === true) {
|
|
4
4
|
if (env.CODEX_MANAGED_BY_NPM === "1" && env.ORACLE_FORCE_OSC_PROGRESS !== "1") {
|
|
5
5
|
return false;
|
|
6
6
|
}
|
|
@@ -16,8 +16,9 @@ export function startOscProgress(options = {}) {
|
|
|
16
16
|
}
|
|
17
17
|
return startOscProgressShared({
|
|
18
18
|
...options,
|
|
19
|
-
// Preserve Oracle's previous
|
|
19
|
+
// Preserve Oracle's previous defaults: progress emits to and checks stdout.
|
|
20
20
|
write: options.write ?? ((text) => process.stdout.write(text)),
|
|
21
|
+
isTty: options.isTty ?? process.stdout.isTTY === true,
|
|
21
22
|
disableEnvVar: "ORACLE_NO_OSC_PROGRESS",
|
|
22
23
|
forceEnvVar: "ORACLE_FORCE_OSC_PROGRESS",
|
|
23
24
|
});
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -25,7 +25,7 @@ import { createMarkdownStreamer } from "markdansi";
|
|
|
25
25
|
import { executeBackgroundResponse } from "./background.js";
|
|
26
26
|
import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from "./runUtils.js";
|
|
27
27
|
import { estimateUsdCost } from "tokentally";
|
|
28
|
-
import { isOpenRouterBaseUrl, isProModel, resolveModelConfig } from "./modelResolver.js";
|
|
28
|
+
import { isOpenRouterBaseUrl, isProModel, resolveModelConfig, resolveOverriddenApiModel, } from "./modelResolver.js";
|
|
29
29
|
import { validateProviderRouting } from "./providerRouting.js";
|
|
30
30
|
import { formatRouteTargetForLog, resolveProviderRoute, } from "./providerRoutePlan.js";
|
|
31
31
|
const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
|
|
@@ -133,6 +133,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
133
133
|
const modelConfig = await resolveModelConfig(options.model, {
|
|
134
134
|
baseUrl,
|
|
135
135
|
openRouterApiKey: resolverOpenRouterApiKey,
|
|
136
|
+
modelOverrides: options.modelOverrides,
|
|
136
137
|
});
|
|
137
138
|
const isLongRunningModel = isProTierModel;
|
|
138
139
|
const supportsBackground = modelConfig.supportsBackground !== false;
|
|
@@ -194,6 +195,8 @@ export async function runOracle(options, deps = {}) {
|
|
|
194
195
|
// Track the concrete model id we dispatch to (especially for Gemini preview aliases)
|
|
195
196
|
const effectiveModelId = azureDeploymentName ??
|
|
196
197
|
options.effectiveModelId ??
|
|
198
|
+
// A user-config apiModel override (known models only) wins over Gemini alias remapping.
|
|
199
|
+
resolveOverriddenApiModel(options.model, options.modelOverrides) ??
|
|
197
200
|
(options.model.startsWith("gemini")
|
|
198
201
|
? resolveGeminiModelId(options.model)
|
|
199
202
|
: (modelConfig.apiModel ?? modelConfig.model));
|