@steipete/oracle 0.8.6 → 0.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.
Files changed (41) hide show
  1. package/README.md +76 -4
  2. package/dist/bin/oracle-cli.js +188 -7
  3. package/dist/src/browser/actions/modelSelection.js +60 -8
  4. package/dist/src/browser/actions/navigation.js +2 -1
  5. package/dist/src/browser/constants.js +1 -1
  6. package/dist/src/browser/index.js +73 -19
  7. package/dist/src/browser/providerDomFlow.js +17 -0
  8. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  9. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +245 -0
  10. package/dist/src/browser/providers/index.js +2 -0
  11. package/dist/src/cli/browserConfig.js +12 -6
  12. package/dist/src/cli/detach.js +5 -2
  13. package/dist/src/cli/fileSize.js +11 -0
  14. package/dist/src/cli/help.js +3 -3
  15. package/dist/src/cli/markdownBundle.js +5 -1
  16. package/dist/src/cli/options.js +40 -3
  17. package/dist/src/cli/runOptions.js +11 -3
  18. package/dist/src/cli/sessionDisplay.js +91 -2
  19. package/dist/src/cli/sessionLineage.js +56 -0
  20. package/dist/src/cli/sessionRunner.js +20 -2
  21. package/dist/src/cli/sessionTable.js +2 -1
  22. package/dist/src/cli/tui/index.js +2 -0
  23. package/dist/src/gemini-web/browserSessionManager.js +76 -0
  24. package/dist/src/gemini-web/client.js +16 -5
  25. package/dist/src/gemini-web/executionClients.js +1 -0
  26. package/dist/src/gemini-web/executionMode.js +18 -0
  27. package/dist/src/gemini-web/executor.js +273 -120
  28. package/dist/src/mcp/tools/consult.js +34 -21
  29. package/dist/src/oracle/client.js +42 -13
  30. package/dist/src/oracle/config.js +43 -7
  31. package/dist/src/oracle/errors.js +2 -2
  32. package/dist/src/oracle/files.js +20 -5
  33. package/dist/src/oracle/gemini.js +3 -0
  34. package/dist/src/oracle/request.js +7 -2
  35. package/dist/src/oracle/run.js +22 -12
  36. package/dist/src/sessionManager.js +4 -0
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  38. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  39. package/package.json +18 -18
  40. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  41. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -1,6 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import { getCookies } from '@steipete/sweet-cookie';
3
+ import { runProviderDomFlow } from '../browser/providerDomFlow.js';
4
+ import { delay } from '../browser/utils.js';
3
5
  import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from './client.js';
6
+ import { geminiDeepThinkDomProvider } from '../browser/providers/index.js';
7
+ import { openGeminiBrowserSession } from './browserSessionManager.js';
8
+ import { selectGeminiExecutionMode } from './executionMode.js';
4
9
  const GEMINI_COOKIE_NAMES = [
5
10
  '__Secure-1PSID',
6
11
  '__Secure-1PSIDTS',
@@ -37,16 +42,21 @@ function resolveGeminiWebModel(desiredModel, log) {
37
42
  const desired = typeof desiredModel === 'string' ? desiredModel.trim() : '';
38
43
  if (!desired)
39
44
  return 'gemini-3-pro';
40
- switch (desired) {
45
+ const normalized = desired.toLowerCase().replace(/[_\s]+/g, '-');
46
+ switch (normalized) {
41
47
  case 'gemini-3-pro':
42
48
  case 'gemini-3.0-pro':
43
49
  return 'gemini-3-pro';
50
+ case 'gemini-3-deep-think':
51
+ case 'gemini-3-pro-deep-think':
52
+ case 'gemini-3-pro-deepthink':
53
+ return 'gemini-3-pro-deep-think';
44
54
  case 'gemini-2.5-pro':
45
55
  return 'gemini-2.5-pro';
46
56
  case 'gemini-2.5-flash':
47
57
  return 'gemini-2.5-flash';
48
58
  default:
49
- if (desired.startsWith('gemini-')) {
59
+ if (normalized.startsWith('gemini-') || normalized.includes('gemini')) {
50
60
  log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to gemini-3-pro.`);
51
61
  }
52
62
  return 'gemini-3-pro';
@@ -91,10 +101,97 @@ function buildGeminiCookieMap(cookies) {
91
101
  function hasRequiredGeminiCookies(cookieMap) {
92
102
  return GEMINI_REQUIRED_COOKIES.every((name) => Boolean(cookieMap[name]));
93
103
  }
104
+ const GEMINI_CDP_COOKIE_URLS = [
105
+ 'https://gemini.google.com',
106
+ 'https://accounts.google.com',
107
+ 'https://www.google.com',
108
+ ];
109
+ async function loadGeminiCookiesFromCDP(browserConfig, log) {
110
+ const session = await openGeminiBrowserSession({
111
+ browserConfig,
112
+ keepBrowserDefault: false,
113
+ purpose: 'Gemini manual-login cookie extraction (no keychain)',
114
+ log,
115
+ });
116
+ try {
117
+ const client = session.client;
118
+ const { Network, Page } = client;
119
+ await Network.enable({});
120
+ await Page.enable();
121
+ log?.('[gemini-web] Navigating to gemini.google.com for sign-in/cookie capture...');
122
+ await Page.navigate({ url: 'https://gemini.google.com' });
123
+ await delay(2_000);
124
+ const pollTimeoutMs = 5 * 60_000;
125
+ const pollIntervalMs = 2_000;
126
+ const deadline = Date.now() + pollTimeoutMs;
127
+ let lastNotice = 0;
128
+ let cookieMap = {};
129
+ while (Date.now() < deadline) {
130
+ const { cookies } = await Network.getCookies({ urls: GEMINI_CDP_COOKIE_URLS });
131
+ cookieMap = buildGeminiCookieMap(cookies);
132
+ if (hasRequiredGeminiCookies(cookieMap)) {
133
+ log?.(`[gemini-web] Extracted ${Object.keys(cookieMap).length} Gemini cookie(s) via CDP.`);
134
+ return { cookieMap, warnings: [] };
135
+ }
136
+ const now = Date.now();
137
+ if (now - lastNotice > 10_000) {
138
+ log?.('[gemini-web] Waiting for Google sign-in... please sign in in the opened Chrome window.');
139
+ lastNotice = now;
140
+ }
141
+ await delay(pollIntervalMs);
142
+ }
143
+ throw new Error('Timed out waiting for Google sign-in (5 minutes). Please sign in and retry.');
144
+ }
145
+ finally {
146
+ await session.close();
147
+ }
148
+ }
149
+ async function runGeminiDeepThinkViaBrowser(prompt, browserConfig, log) {
150
+ const session = await openGeminiBrowserSession({
151
+ browserConfig,
152
+ keepBrowserDefault: true,
153
+ purpose: 'Gemini Deep Think',
154
+ log,
155
+ });
156
+ try {
157
+ const client = session.client;
158
+ const { Runtime, Page } = client;
159
+ if (!Runtime || typeof Runtime.enable !== 'function' || typeof Runtime.evaluate !== 'function') {
160
+ throw new Error('Chrome Runtime domain unavailable for Gemini Deep Think DOM automation.');
161
+ }
162
+ if (!Page || typeof Page.enable !== 'function' || typeof Page.navigate !== 'function') {
163
+ throw new Error('Chrome Page domain unavailable for Gemini Deep Think DOM automation.');
164
+ }
165
+ await Runtime.enable();
166
+ await Page.enable();
167
+ const evaluate = async (expression) => {
168
+ const { result } = await Runtime.evaluate({ expression, returnByValue: true });
169
+ return result?.value;
170
+ };
171
+ log?.('[gemini-web] Navigating to gemini.google.com...');
172
+ await Page.navigate({ url: 'https://gemini.google.com/app' });
173
+ await delay(3_000);
174
+ const domResult = await runProviderDomFlow(geminiDeepThinkDomProvider, {
175
+ prompt,
176
+ evaluate,
177
+ delay,
178
+ log,
179
+ state: {
180
+ inputTimeoutMs: browserConfig?.inputTimeoutMs,
181
+ timeoutMs: browserConfig?.timeoutMs,
182
+ },
183
+ });
184
+ log?.(`[gemini-web] Deep Think response received (${domResult.text.length} chars).`);
185
+ return domResult;
186
+ }
187
+ finally {
188
+ await session.close();
189
+ }
190
+ }
94
191
  async function loadGeminiCookiesFromInline(browserConfig, log) {
95
192
  const inline = browserConfig?.inlineCookies;
96
193
  if (!inline || inline.length === 0)
97
- return {};
194
+ return { cookieMap: {}, warnings: [] };
98
195
  const cookieMap = buildGeminiCookieMap(inline.filter((cookie) => Boolean(cookie?.name && typeof cookie.value === 'string')));
99
196
  if (Object.keys(cookieMap).length > 0) {
100
197
  const source = browserConfig?.inlineCookiesSource ?? 'inline';
@@ -103,7 +200,7 @@ async function loadGeminiCookiesFromInline(browserConfig, log) {
103
200
  else {
104
201
  log?.('[gemini-web] Inline cookie payload provided but no Gemini cookies matched.');
105
202
  }
106
- return cookieMap;
203
+ return { cookieMap, warnings: [] };
107
204
  }
108
205
  async function loadGeminiCookiesFromChrome(browserConfig, log) {
109
206
  try {
@@ -131,47 +228,52 @@ async function loadGeminiCookiesFromChrome(browserConfig, log) {
131
228
  }
132
229
  const cookieMap = buildGeminiCookieMap(cookies);
133
230
  log?.(`[gemini-web] Loaded Gemini cookies from Chrome (node): ${Object.keys(cookieMap).length} cookie(s).`);
134
- return cookieMap;
231
+ return { cookieMap, warnings };
135
232
  }
136
233
  catch (error) {
137
234
  log?.(`[gemini-web] Failed to load Chrome cookies via node: ${error instanceof Error ? error.message : String(error ?? '')}`);
138
- return {};
235
+ return { cookieMap: {}, warnings: [] };
236
+ }
237
+ }
238
+ function formatGeminiCookieError(warnings) {
239
+ const base = 'Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).';
240
+ const guidance = 'Try --browser-manual-login or --browser-inline-cookies-file if local cookie extraction is unavailable.';
241
+ if (warnings.length === 0) {
242
+ return `${base} ${guidance}`;
139
243
  }
244
+ return `${base}\nCookie read warnings:\n- ${warnings.join('\n- ')}\n${guidance}`;
140
245
  }
141
- async function loadGeminiCookies(browserConfig, log) {
142
- const inlineMap = await loadGeminiCookiesFromInline(browserConfig, log);
143
- const hasInlineRequired = hasRequiredGeminiCookies(inlineMap);
144
- if (hasInlineRequired && browserConfig?.cookieSync === false) {
145
- return inlineMap;
246
+ async function loadGeminiCookies(browserConfig, log, options) {
247
+ const inlineResult = await loadGeminiCookiesFromInline(browserConfig, log);
248
+ const hasInlineRequired = hasRequiredGeminiCookies(inlineResult.cookieMap);
249
+ if (hasInlineRequired) {
250
+ return inlineResult;
251
+ }
252
+ const manualNoKeychain = Boolean(browserConfig?.manualLogin) || Boolean(options?.preferManualNoKeychain);
253
+ if (manualNoKeychain) {
254
+ log?.('[gemini-web] Using manual-login cookie extraction path (no keychain cookie read).');
255
+ const cdpResult = await loadGeminiCookiesFromCDP(browserConfig, log);
256
+ return {
257
+ cookieMap: { ...cdpResult.cookieMap, ...inlineResult.cookieMap },
258
+ warnings: [...inlineResult.warnings, ...cdpResult.warnings],
259
+ };
146
260
  }
147
261
  if (browserConfig?.cookieSync === false && !hasInlineRequired) {
148
262
  log?.('[gemini-web] Cookie sync disabled and inline cookies missing Gemini auth tokens.');
149
- return inlineMap;
263
+ return inlineResult;
150
264
  }
151
- const chromeMap = await loadGeminiCookiesFromChrome(browserConfig, log);
152
- const merged = { ...chromeMap, ...inlineMap };
153
- return merged;
265
+ const chromeResult = await loadGeminiCookiesFromChrome(browserConfig, log);
266
+ return {
267
+ cookieMap: { ...chromeResult.cookieMap, ...inlineResult.cookieMap },
268
+ warnings: [...inlineResult.warnings, ...chromeResult.warnings],
269
+ };
154
270
  }
155
271
  export function createGeminiWebExecutor(geminiOptions) {
156
272
  return async (runOptions) => {
157
273
  const startTime = Date.now();
158
274
  const log = runOptions.log;
159
275
  log?.('[gemini-web] Starting Gemini web executor (TypeScript)');
160
- const cookieMap = await loadGeminiCookies(runOptions.config, log);
161
- if (!hasRequiredGeminiCookies(cookieMap)) {
162
- throw new Error('Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).');
163
- }
164
- const configTimeout = typeof runOptions.config?.timeoutMs === 'number' && Number.isFinite(runOptions.config.timeoutMs)
165
- ? Math.max(1_000, runOptions.config.timeoutMs)
166
- : null;
167
- const defaultTimeoutMs = geminiOptions.youtube
168
- ? 240_000
169
- : geminiOptions.generateImage || geminiOptions.editImage
170
- ? 300_000
171
- : 120_000;
172
- const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
173
- const controller = new AbortController();
174
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
276
+ const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
175
277
  const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
176
278
  const editImagePath = resolveInvocationPath(geminiOptions.editImage);
177
279
  const outputPath = resolveInvocationPath(geminiOptions.outputPath);
@@ -186,100 +288,151 @@ export function createGeminiWebExecutor(geminiOptions) {
186
288
  if (generateImagePath && !editImagePath) {
187
289
  prompt = `Generate an image: ${prompt}`;
188
290
  }
189
- const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
190
- let response;
191
- try {
192
- if (editImagePath) {
193
- const intro = await runGeminiWebWithFallback({
194
- prompt: 'Here is an image to edit',
195
- files: [editImagePath],
196
- model,
197
- cookieMap,
198
- chatMetadata: null,
199
- signal: controller.signal,
200
- });
201
- const editPrompt = `Use image generation tool to ${prompt}`;
202
- const out = await runGeminiWebWithFallback({
203
- prompt: editPrompt,
204
- files: attachmentPaths,
205
- model,
206
- cookieMap,
207
- chatMetadata: intro.metadata,
208
- signal: controller.signal,
209
- });
210
- response = {
211
- text: out.text ?? null,
212
- thoughts: geminiOptions.showThoughts ? out.thoughts : null,
213
- has_images: false,
214
- image_count: 0,
215
- };
216
- const resolvedOutputPath = outputPath ?? generateImagePath ?? 'generated.png';
217
- const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, resolvedOutputPath, controller.signal);
218
- response.has_images = imageSave.saved;
219
- response.image_count = imageSave.imageCount;
220
- if (!imageSave.saved) {
221
- throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
291
+ const modeSelection = selectGeminiExecutionMode({
292
+ model,
293
+ attachmentPaths,
294
+ generateImagePath,
295
+ editImagePath,
296
+ });
297
+ const domClient = {
298
+ mode: 'dom',
299
+ execute: async () => {
300
+ log?.('[gemini-web] Using browser DOM automation for Deep Think.');
301
+ const browserResult = await runGeminiDeepThinkViaBrowser(prompt, runOptions.config, log);
302
+ const tookMs = Date.now() - startTime;
303
+ let answerMarkdown = browserResult.text;
304
+ if (geminiOptions.showThoughts && browserResult.thoughts) {
305
+ answerMarkdown = `## Thinking\n\n${browserResult.thoughts}\n\n## Response\n\n${browserResult.text}`;
222
306
  }
223
- }
224
- else if (generateImagePath) {
225
- const out = await runGeminiWebWithFallback({
226
- prompt,
227
- files: attachmentPaths,
228
- model,
229
- cookieMap,
230
- chatMetadata: null,
231
- signal: controller.signal,
232
- });
233
- response = {
234
- text: out.text ?? null,
235
- thoughts: geminiOptions.showThoughts ? out.thoughts : null,
236
- has_images: false,
237
- image_count: 0,
307
+ log?.(`[gemini-web] Completed in ${tookMs}ms`);
308
+ return {
309
+ answerText: browserResult.text,
310
+ answerMarkdown,
311
+ tookMs,
312
+ answerTokens: estimateTokenCount(browserResult.text),
313
+ answerChars: browserResult.text.length,
238
314
  };
239
- const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, generateImagePath, controller.signal);
240
- response.has_images = imageSave.saved;
241
- response.image_count = imageSave.imageCount;
242
- if (!imageSave.saved) {
243
- throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
315
+ },
316
+ };
317
+ const httpClient = {
318
+ mode: 'http',
319
+ execute: async () => {
320
+ const useNoKeychainPath = Boolean(runOptions.config?.manualLogin);
321
+ const cookieResult = await loadGeminiCookies(runOptions.config, log, { preferManualNoKeychain: useNoKeychainPath });
322
+ if (!hasRequiredGeminiCookies(cookieResult.cookieMap)) {
323
+ throw new Error(formatGeminiCookieError(cookieResult.warnings));
244
324
  }
245
- }
246
- else {
247
- const out = await runGeminiWebWithFallback({
248
- prompt,
249
- files: attachmentPaths,
250
- model,
251
- cookieMap,
252
- chatMetadata: null,
253
- signal: controller.signal,
254
- });
255
- response = {
256
- text: out.text ?? null,
257
- thoughts: geminiOptions.showThoughts ? out.thoughts : null,
258
- has_images: out.images.length > 0,
259
- image_count: out.images.length,
325
+ const configTimeout = typeof runOptions.config?.timeoutMs === 'number' && Number.isFinite(runOptions.config.timeoutMs)
326
+ ? Math.max(1_000, runOptions.config.timeoutMs)
327
+ : null;
328
+ const defaultTimeoutMs = geminiOptions.youtube
329
+ ? 240_000
330
+ : geminiOptions.generateImage || geminiOptions.editImage
331
+ ? 300_000
332
+ : 120_000;
333
+ const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
334
+ const controller = new AbortController();
335
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
336
+ let response;
337
+ try {
338
+ if (editImagePath) {
339
+ const intro = await runGeminiWebWithFallback({
340
+ prompt: 'Here is an image to edit',
341
+ files: [editImagePath],
342
+ model,
343
+ cookieMap: cookieResult.cookieMap,
344
+ chatMetadata: null,
345
+ signal: controller.signal,
346
+ });
347
+ const editPrompt = `Use image generation tool to ${prompt}`;
348
+ const out = await runGeminiWebWithFallback({
349
+ prompt: editPrompt,
350
+ files: attachmentPaths,
351
+ model,
352
+ cookieMap: cookieResult.cookieMap,
353
+ chatMetadata: intro.metadata,
354
+ signal: controller.signal,
355
+ });
356
+ response = {
357
+ text: out.text ?? null,
358
+ thoughts: geminiOptions.showThoughts ? out.thoughts : null,
359
+ has_images: false,
360
+ image_count: 0,
361
+ };
362
+ const resolvedOutputPath = outputPath ?? generateImagePath ?? 'generated.png';
363
+ const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, resolvedOutputPath, controller.signal);
364
+ response.has_images = imageSave.saved;
365
+ response.image_count = imageSave.imageCount;
366
+ if (!imageSave.saved) {
367
+ throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
368
+ }
369
+ }
370
+ else if (generateImagePath) {
371
+ const out = await runGeminiWebWithFallback({
372
+ prompt,
373
+ files: attachmentPaths,
374
+ model,
375
+ cookieMap: cookieResult.cookieMap,
376
+ chatMetadata: null,
377
+ signal: controller.signal,
378
+ });
379
+ response = {
380
+ text: out.text ?? null,
381
+ thoughts: geminiOptions.showThoughts ? out.thoughts : null,
382
+ has_images: false,
383
+ image_count: 0,
384
+ };
385
+ const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, generateImagePath, controller.signal);
386
+ response.has_images = imageSave.saved;
387
+ response.image_count = imageSave.imageCount;
388
+ if (!imageSave.saved) {
389
+ throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
390
+ }
391
+ }
392
+ else {
393
+ const out = await runGeminiWebWithFallback({
394
+ prompt,
395
+ files: attachmentPaths,
396
+ model,
397
+ cookieMap: cookieResult.cookieMap,
398
+ chatMetadata: null,
399
+ signal: controller.signal,
400
+ });
401
+ response = {
402
+ text: out.text ?? null,
403
+ thoughts: geminiOptions.showThoughts ? out.thoughts : null,
404
+ has_images: out.images.length > 0,
405
+ image_count: out.images.length,
406
+ };
407
+ }
408
+ }
409
+ finally {
410
+ clearTimeout(timeout);
411
+ }
412
+ const answerText = response.text ?? '';
413
+ let answerMarkdown = answerText;
414
+ if (geminiOptions.showThoughts && response.thoughts) {
415
+ answerMarkdown = `## Thinking\n\n${response.thoughts}\n\n## Response\n\n${answerText}`;
416
+ }
417
+ if (response.has_images && response.image_count > 0) {
418
+ const imagePath = generateImagePath || outputPath || 'generated.png';
419
+ answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
420
+ }
421
+ const tookMs = Date.now() - startTime;
422
+ log?.(`[gemini-web] Completed in ${tookMs}ms`);
423
+ return {
424
+ answerText,
425
+ answerMarkdown,
426
+ tookMs,
427
+ answerTokens: estimateTokenCount(answerText),
428
+ answerChars: answerText.length,
260
429
  };
261
- }
262
- }
263
- finally {
264
- clearTimeout(timeout);
265
- }
266
- const answerText = response.text ?? '';
267
- let answerMarkdown = answerText;
268
- if (geminiOptions.showThoughts && response.thoughts) {
269
- answerMarkdown = `## Thinking\n\n${response.thoughts}\n\n## Response\n\n${answerText}`;
270
- }
271
- if (response.has_images && response.image_count > 0) {
272
- const imagePath = generateImagePath || outputPath || 'generated.png';
273
- answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
274
- }
275
- const tookMs = Date.now() - startTime;
276
- log?.(`[gemini-web] Completed in ${tookMs}ms`);
277
- return {
278
- answerText,
279
- answerMarkdown,
280
- tookMs,
281
- answerTokens: estimateTokenCount(answerText),
282
- answerChars: answerText.length,
430
+ },
283
431
  };
432
+ if (model === 'gemini-3-pro-deep-think' && modeSelection.mode === 'http') {
433
+ log?.(`[gemini-web] Deep Think DOM path skipped (${modeSelection.reasons.join(', ')} requested); using HTTP/header fallback path.`);
434
+ }
435
+ const executionClient = modeSelection.mode === 'dom' ? domClient : httpClient;
436
+ return executionClient.execute();
284
437
  };
285
438
  }
@@ -139,6 +139,31 @@ export function summarizeModelRunsForConsult(runs) {
139
139
  };
140
140
  });
141
141
  }
142
+ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputModel, browserModelLabel, browserThinkingTime, browserKeepBrowser, }) {
143
+ const configuredBrowser = userConfig.browser ?? {};
144
+ const envProfileDir = (env.ORACLE_BROWSER_PROFILE_DIR ?? '').trim();
145
+ const hasProfileDir = envProfileDir.length > 0;
146
+ const preferredLabel = (browserModelLabel ?? inputModel)?.trim();
147
+ const isChatGptModel = runModel.startsWith('gpt-') && !runModel.includes('codex');
148
+ const desiredModelLabel = isChatGptModel
149
+ ? mapModelToBrowserLabel(runModel)
150
+ : resolveBrowserModelLabel(preferredLabel, runModel);
151
+ const configuredUrl = configuredBrowser.chatgptUrl ?? configuredBrowser.url ?? CHATGPT_URL;
152
+ const manualLogin = hasProfileDir ? true : configuredBrowser.manualLogin ?? false;
153
+ return {
154
+ ...configuredBrowser,
155
+ url: configuredUrl,
156
+ chatgptUrl: configuredUrl,
157
+ cookieSync: !manualLogin,
158
+ headless: configuredBrowser.headless ?? false,
159
+ hideWindow: configuredBrowser.hideWindow ?? false,
160
+ keepBrowser: browserKeepBrowser ?? configuredBrowser.keepBrowser ?? false,
161
+ manualLogin,
162
+ manualLoginProfileDir: manualLogin ? ((envProfileDir || configuredBrowser.manualLoginProfileDir) ?? null) : null,
163
+ thinkingTime: browserThinkingTime ?? configuredBrowser.thinkingTime,
164
+ desiredModel: desiredModelLabel || mapModelToBrowserLabel(runModel),
165
+ };
166
+ }
142
167
  export function registerConsultTool(server) {
143
168
  server.registerTool('consult', {
144
169
  title: 'Run an oracle session',
@@ -185,27 +210,15 @@ export function registerConsultTool(server) {
185
210
  }
186
211
  let browserConfig;
187
212
  if (resolvedEngine === 'browser') {
188
- const envProfileDir = (process.env.ORACLE_BROWSER_PROFILE_DIR ?? '').trim();
189
- const hasProfileDir = envProfileDir.length > 0;
190
- const preferredLabel = (browserModelLabel ?? model)?.trim();
191
- const isChatGptModel = runOptions.model.startsWith('gpt-') && !runOptions.model.includes('codex');
192
- const desiredModelLabel = isChatGptModel
193
- ? mapModelToBrowserLabel(runOptions.model)
194
- : resolveBrowserModelLabel(preferredLabel, runOptions.model);
195
- const configuredUrl = userConfig.browser?.chatgptUrl ?? userConfig.browser?.url ?? undefined;
196
- // Default to manual-login when a persistent profile dir is provided (common for Codex/Claude).
197
- const manualLogin = hasProfileDir;
198
- browserConfig = {
199
- url: configuredUrl ?? CHATGPT_URL,
200
- cookieSync: !manualLogin,
201
- headless: false,
202
- hideWindow: false,
203
- keepBrowser: browserKeepBrowser ?? false,
204
- manualLogin,
205
- manualLoginProfileDir: manualLogin ? envProfileDir : null,
206
- thinkingTime: browserThinkingTime,
207
- desiredModel: desiredModelLabel || mapModelToBrowserLabel(runOptions.model),
208
- };
213
+ browserConfig = buildConsultBrowserConfig({
214
+ userConfig,
215
+ env: process.env,
216
+ runModel: runOptions.model,
217
+ inputModel: model,
218
+ browserModelLabel,
219
+ browserThinkingTime,
220
+ browserKeepBrowser,
221
+ });
209
222
  }
210
223
  const notifications = resolveNotificationSettings({
211
224
  cliNotify: undefined,
@@ -1,34 +1,63 @@
1
- import OpenAI, { AzureOpenAI } from 'openai';
1
+ import OpenAI from 'openai';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { createGeminiClient } from './gemini.js';
5
5
  import { createClaudeClient } from './claude.js';
6
6
  import { isOpenRouterBaseUrl } from './modelResolver.js';
7
+ /**
8
+ * Known native API base URLs that should still use their dedicated SDKs.
9
+ * Any other custom base URL is treated as an OpenAI-compatible proxy and
10
+ * all models are routed through the chat/completions adapter.
11
+ */
12
+ const NATIVE_API_HOSTS = [
13
+ 'api.openai.com',
14
+ 'api.anthropic.com',
15
+ 'generativelanguage.googleapis.com',
16
+ 'api.x.ai',
17
+ ];
18
+ export function isCustomBaseUrl(baseUrl) {
19
+ if (!baseUrl)
20
+ return false;
21
+ try {
22
+ const url = new URL(baseUrl);
23
+ return !NATIVE_API_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ export function buildAzureResponsesBaseUrl(endpoint) {
30
+ return `${endpoint.replace(/\/+$/, '')}/openai/v1`;
31
+ }
7
32
  export function createDefaultClientFactory() {
8
33
  const customFactory = loadCustomClientFactory();
9
34
  if (customFactory)
10
35
  return customFactory;
11
36
  return (key, options) => {
12
- if (options?.model?.startsWith('gemini')) {
13
- // Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
14
- return createGeminiClient(key, options.model, options.resolvedModelId);
15
- }
16
- if (options?.model?.startsWith('claude')) {
17
- return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
37
+ const openRouter = isOpenRouterBaseUrl(options?.baseUrl);
38
+ const customProxy = isCustomBaseUrl(options?.baseUrl);
39
+ // When using any custom/proxy base URL (OpenRouter, LiteLLM, vLLM, Together, etc.),
40
+ // route ALL models through the OpenAI chat/completions adapter instead of native SDKs
41
+ // which would reject the proxy's API key.
42
+ if (!openRouter && !customProxy) {
43
+ if (options?.model?.startsWith('gemini')) {
44
+ // Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
45
+ return createGeminiClient(key, options.model, options.resolvedModelId);
46
+ }
47
+ if (options?.model?.startsWith('claude')) {
48
+ return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
49
+ }
18
50
  }
19
51
  let instance;
20
- const openRouter = isOpenRouterBaseUrl(options?.baseUrl);
21
52
  const defaultHeaders = openRouter ? buildOpenRouterHeaders() : undefined;
22
53
  const httpTimeoutMs = typeof options?.httpTimeoutMs === 'number' && Number.isFinite(options.httpTimeoutMs) && options.httpTimeoutMs > 0
23
54
  ? options.httpTimeoutMs
24
55
  : 20 * 60 * 1000;
25
56
  if (options?.azure?.endpoint) {
26
- instance = new AzureOpenAI({
57
+ instance = new OpenAI({
27
58
  apiKey: key,
28
- endpoint: options.azure.endpoint,
29
- apiVersion: options.azure.apiVersion,
30
- deployment: options.azure.deployment,
31
59
  timeout: httpTimeoutMs,
60
+ baseURL: buildAzureResponsesBaseUrl(options.azure.endpoint),
32
61
  });
33
62
  }
34
63
  else {
@@ -39,7 +68,7 @@ export function createDefaultClientFactory() {
39
68
  defaultHeaders,
40
69
  });
41
70
  }
42
- if (openRouter) {
71
+ if (openRouter || customProxy) {
43
72
  return buildOpenRouterCompletionClient(instance);
44
73
  }
45
74
  return {