diff --git a/README.md b/README.md index eb660bf..c8287ef 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The node uses one Browserbase credential: | --- | --- | | `Browserbase API Key` | Required for all resources | | `Browserbase Project ID (Deprecated)` | Optional legacy header | -| `Model API Key` | Optional. Only needed for Agent when using your own model provider key | +| `Anthropic / OpenAI / Google API Key` | Optional. Bring your own model API key per provider for the Agent. Leave all empty to use the Browserbase Model Gateway. | ## Example Usage diff --git a/credentials/BrowserbaseApi.credentials.ts b/credentials/BrowserbaseApi.credentials.ts index e11025e..65ed981 100644 --- a/credentials/BrowserbaseApi.credentials.ts +++ b/credentials/BrowserbaseApi.credentials.ts @@ -44,13 +44,44 @@ export class BrowserbaseApi implements ICredentialType { description: 'Optional. Your Browserbase project ID (no longer required for new setups)', }, { - displayName: 'Model API Key', + displayName: 'Anthropic API Key', + name: 'anthropicApiKey', + type: 'string', + typeOptions: { password: true }, + default: '', + required: false, + description: + 'Optional. Bring your own Anthropic key for the Agent. Leave all model keys empty to use the Browserbase Model Gateway.', + }, + { + displayName: 'OpenAI API Key', + name: 'openAiApiKey', + type: 'string', + typeOptions: { password: true }, + default: '', + required: false, + description: + 'Optional. Bring your own OpenAI key for the Agent. Leave all model keys empty to use the Browserbase Model Gateway.', + }, + { + displayName: 'Google API Key', + name: 'googleApiKey', + type: 'string', + typeOptions: { password: true }, + default: '', + required: false, + description: + 'Optional. Bring your own Google (Gemini) key for the Agent. Leave all model keys empty to use the Browserbase Model Gateway.', + }, + { + displayName: 'Model API Key (Deprecated)', name: 'modelApiKey', type: 'string', typeOptions: { password: true }, default: '', required: false, - description: 'Optional. Provide your own model API key, or leave blank to use the Browserbase Model Gateway.', + description: + 'Deprecated. Use the provider-specific keys above instead. Kept for credentials created before per-provider keys existed; used as a fallback when no matching provider key is set.', }, ]; diff --git a/nodes/Browserbase/Browserbase.node.ts b/nodes/Browserbase/Browserbase.node.ts index a21926f..0446e1a 100644 --- a/nodes/Browserbase/Browserbase.node.ts +++ b/nodes/Browserbase/Browserbase.node.ts @@ -7,6 +7,7 @@ import { type INodeCredentialTestResult, type INodeExecutionData, type INodeProperties, + type INodePropertyOptions, type INodeType, type INodeTypeDescription, type IHttpRequestMethods, @@ -16,6 +17,59 @@ import { const STAGEHAND_BASE_URL = 'https://api.stagehand.browserbase.com'; const API_BASE_URL = 'https://api.browserbase.com'; +// Single source of truth for CUA "Agent Model" options — mirrors Stagehand's +// AVAILABLE_CUA_MODELS +const CUA_MODEL_OPTIONS: INodePropertyOptions[] = [ + { name: 'Claude Fable 5 (Anthropic)', value: 'anthropic/claude-fable-5' }, + { + name: 'Claude Haiku 4.5 (2025-10-01, Anthropic)', + value: 'anthropic/claude-haiku-4-5-20251001', + }, + { name: 'Claude Haiku 4.5 (Anthropic)', value: 'anthropic/claude-haiku-4-5' }, + { + name: 'Claude Opus 4.5 (2025-11-01, Anthropic)', + value: 'anthropic/claude-opus-4-5-20251101', + }, + { name: 'Claude Opus 4.6 (Anthropic)', value: 'anthropic/claude-opus-4-6' }, + { name: 'Claude Opus 4.8 (Anthropic)', value: 'anthropic/claude-opus-4-8' }, + { + name: 'Claude Sonnet 4 (2025-05-14, Anthropic)', + value: 'anthropic/claude-sonnet-4-20250514', + }, + { + name: 'Claude Sonnet 4.5 (2025-09-29, Anthropic)', + value: 'anthropic/claude-sonnet-4-5-20250929', + }, + { name: 'Claude Sonnet 4.6 (Anthropic)', value: 'anthropic/claude-sonnet-4-6' }, + { + name: 'Computer Use Preview (2025-03-11, OpenAI)', + value: 'openai/computer-use-preview-2025-03-11', + }, + { name: 'Computer Use Preview (OpenAI)', value: 'openai/computer-use-preview' }, + { name: 'Fara 7B (Microsoft)', value: 'microsoft/fara-7b' }, + { name: 'Gemini 2.5 CUA (Google)', value: 'google/gemini-2.5-computer-use-preview-10-2025' }, + { name: 'Gemini 3 Flash (Google)', value: 'google/gemini-3-flash-preview' }, + { name: 'Gemini 3 Pro (Google)', value: 'google/gemini-3-pro-preview' }, + { name: 'Gemini 3.5 Flash (Google)', value: 'google/gemini-3.5-flash' }, + { name: 'GPT-5.4 (OpenAI)', value: 'openai/gpt-5.4' }, + { name: 'GPT-5.4 Mini (OpenAI)', value: 'openai/gpt-5.4-mini' }, + { name: 'GPT-5.5 (OpenAI)', value: 'openai/gpt-5.5' }, +]; + +const CUA_MODELS = new Set(CUA_MODEL_OPTIONS.map((option) => option.value)); + +// Curated "Agent Model" options for Hybrid mode (must support coordinate actions). +// Shared by the v3 and legacy modelHybrid dropdowns. +const HYBRID_MODEL_OPTIONS: INodePropertyOptions[] = [ + { name: 'Claude Haiku 4.5 (Anthropic)', value: 'anthropic/claude-haiku-4-5' }, + { name: 'Claude Opus 4.8 (Anthropic)', value: 'anthropic/claude-opus-4-8' }, + { name: 'Claude Sonnet 4.6 (Anthropic)', value: 'anthropic/claude-sonnet-4-6' }, + { name: 'Gemini 3 Flash (Google)', value: 'google/gemini-3-flash-preview' }, + { name: 'Gemini 3 Pro (Google)', value: 'google/gemini-3-pro-preview' }, + { name: 'GPT-5.4 Mini (OpenAI)', value: 'openai/gpt-5.4-mini' }, + { name: 'GPT-5.5 (OpenAI)', value: 'openai/gpt-5.5' }, +]; + type BrowserbaseHeaders = Record; type BrowserOptions = { @@ -63,7 +117,7 @@ function getSessionId(response: Record): string | undefined { function getHeaders( credentials: ICredentialDataDecryptedObject, options?: { - includeModelApiKey?: boolean; + modelApiKey?: string; }, ): BrowserbaseHeaders { const headers: BrowserbaseHeaders = { @@ -72,8 +126,8 @@ function getHeaders( 'x-bb-api-key': credentials.browserbaseApiKey as string, }; - if (options?.includeModelApiKey) { - headers['x-model-api-key'] = credentials.modelApiKey as string; + if (options?.modelApiKey) { + headers['x-model-api-key'] = options.modelApiKey; } const projectId = (credentials.browserbaseProjectId as string)?.trim(); @@ -84,13 +138,63 @@ function getHeaders( return headers; } +function resolveModelApiKey( + credentials: ICredentialDataDecryptedObject, + provider: string, +): string { + const perProvider: Record = { + anthropic: credentials.anthropicApiKey, + openai: credentials.openAiApiKey, + google: credentials.googleApiKey, + }; + // Fall back to the legacy single `modelApiKey` field for credentials saved before per-provider keys existed. + return ((perProvider[provider] as string) || (credentials.modelApiKey as string) || '').trim(); +} + function buildProperties(): INodeProperties[] { return [ + { + displayName: 'Action', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + '@version': [{ _cnd: { gte: 3 } }], + }, + }, + options: [ + { + name: 'Run an Agent', + value: 'execute', + description: 'Run an AI agent to perform browser automation tasks', + action: 'Run an agent', + }, + { + name: 'Fetch a Webpage', + value: 'fetch', + description: 'Fetch a page without starting a browser session', + action: 'Fetch a webpage', + }, + { + name: 'Search the Web', + value: 'search', + description: 'Search the web and return structured results', + action: 'Search the web', + }, + ], + default: 'execute', + }, { displayName: 'Resource', name: 'resource', type: 'options', noDataExpression: true, + displayOptions: { + show: { + '@version': [{ _cnd: { lt: 3 } }], + }, + }, options: [ { name: 'Agent', @@ -114,15 +218,16 @@ function buildProperties(): INodeProperties[] { noDataExpression: true, displayOptions: { show: { + '@version': [{ _cnd: { lt: 3 } }], resource: ['agent'], }, }, options: [ { - name: 'Execute', + name: 'Run an Agent', value: 'execute', - description: 'Execute an AI agent to perform browser automation tasks', - action: 'Execute an agent', + description: 'Run an AI agent to perform browser automation tasks', + action: 'Run an agent', }, ], default: 'execute', @@ -134,6 +239,7 @@ function buildProperties(): INodeProperties[] { noDataExpression: true, displayOptions: { show: { + '@version': [{ _cnd: { lt: 3 } }], resource: ['fetch'], }, }, @@ -154,6 +260,7 @@ function buildProperties(): INodeProperties[] { noDataExpression: true, displayOptions: { show: { + '@version': [{ _cnd: { lt: 3 } }], resource: ['search'], }, }, @@ -168,18 +275,48 @@ function buildProperties(): INodeProperties[] { default: 'search', }, { - displayName: 'Mode Info', - name: 'modeNotice', - type: 'notice', - default: '', + displayName: 'Model', + name: 'driverModel', + type: 'options', displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { gte: 3 } }], operation: ['execute'], }, }, + options: [ + { + name: 'Claude Haiku 4.5 (Anthropic)', + value: 'anthropic/claude-haiku-4-5', + }, + { + name: 'Claude Opus 4.6 (Anthropic)', + value: 'anthropic/claude-opus-4-6', + }, + { + name: 'Claude Sonnet 4.6 (Anthropic)', + value: 'anthropic/claude-sonnet-4-6', + }, + { + name: 'Gemini 3 Flash (Google)', + value: 'google/gemini-3-flash-preview', + }, + { + name: 'Gemini 3 Pro (Google)', + value: 'google/gemini-3-pro-preview', + }, + { + name: 'GPT-4o (OpenAI)', + value: 'openai/gpt-4o', + }, + { + name: 'GPT-4o Mini (OpenAI)', + value: 'openai/gpt-4o-mini', + }, + ], + default: 'google/gemini-3-flash-preview', description: - 'CUA uses vision/coordinates (best for complex UIs). DOM uses selectors (faster, any LLM). Hybrid combines both.', + 'The model that drives the browser and runs the agent. Handles both navigation and reasoning by default. To use a different reasoning model, set "Agent Model" in Model Options. See Stagehand model evals to compare models.', }, { displayName: 'Starting URL', @@ -191,7 +328,6 @@ function buildProperties(): INodeProperties[] { description: 'The starting page URL for the agent', displayOptions: { show: { - resource: ['agent'], operation: ['execute'], }, }, @@ -209,11 +345,163 @@ function buildProperties(): INodeProperties[] { description: 'The task for the agent to complete', displayOptions: { show: { - resource: ['agent'], operation: ['execute'], }, }, }, + { + displayName: 'Model Options', + name: 'modelOptions', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + '@version': [{ _cnd: { gte: 3 } }], + operation: ['execute'], + }, + }, + options: [ + { + displayName: 'Agent Model', + name: 'modelCua', + type: 'options', + displayOptions: { + show: { + mode: ['cua'], + }, + }, + options: CUA_MODEL_OPTIONS, + default: 'google/gemini-3-flash-preview', + description: + 'Optional. Overrides the reasoning model for CUA mode. Defaults to the Model above. See Stagehand model evals to compare models.', + }, + { + displayName: 'Agent Model', + name: 'modelDom', + type: 'options', + displayOptions: { + show: { + mode: ['dom'], + }, + }, + options: [ + { + name: 'Claude Sonnet 4.6 (Anthropic)', + value: 'anthropic/claude-sonnet-4-6', + }, + { + name: 'Gemini 3 Flash (Google)', + value: 'google/gemini-3-flash-preview', + }, + { + name: 'Gemini 3 Pro (Google)', + value: 'google/gemini-3-pro-preview', + }, + { + name: 'GPT-4.1 (OpenAI)', + value: 'openai/gpt-4.1', + }, + { + name: 'GPT-4o (OpenAI)', + value: 'openai/gpt-4o', + }, + { + name: 'GPT-4o Mini (OpenAI) - Budget', + value: 'openai/gpt-4o-mini', + }, + ], + default: 'google/gemini-3-flash-preview', + description: + 'Optional. Overrides the reasoning model for DOM mode. Defaults to the Model above. See Stagehand model evals to compare models.', + }, + { + displayName: 'Agent Model', + name: 'modelHybrid', + type: 'options', + displayOptions: { + show: { + mode: ['hybrid'], + }, + }, + options: HYBRID_MODEL_OPTIONS, + default: 'google/gemini-3-flash-preview', + description: + 'Optional. Overrides the reasoning model for Hybrid mode (must support coordinate actions). Defaults to the Model above. See Stagehand model evals to compare models.', + }, + { + displayName: 'Highlight Cursor', + name: 'highlightCursor', + type: 'boolean', + default: true, + description: 'Whether to highlight the cursor during execution (CUA/Hybrid only)', + }, + { + displayName: 'Max Steps', + name: 'maxSteps', + type: 'number', + default: 20, + description: 'Maximum number of steps the agent can take', + }, + { + displayName: 'Mode', + name: 'mode', + type: 'options', + options: [ + { + name: 'CUA (Computer Use Agent)', + value: 'cua', + description: 'Uses vision and coordinates. Works with CUA-specific models.', + }, + { + name: 'DOM', + value: 'dom', + description: 'Uses DOM selectors. Works with any LLM. Faster.', + }, + { + name: 'Hybrid (Experimental)', + value: 'hybrid', + description: 'Combines vision and DOM. Requires specific models.', + }, + ], + default: 'cua', + description: + 'How the agent interacts with pages. CUA uses vision/coordinates (best for complex UIs). DOM uses selectors (faster, works with any LLM). Hybrid combines both. How to pick a mode.', + }, + { + displayName: 'Model Source', + name: 'modelSource', + type: 'options', + options: [ + { + name: 'Model Gateway (Browserbase)', + value: 'gateway', + description: 'Use Browserbase-managed model routing. Mix any providers freely.', + }, + { + name: 'User-Provided API Key', + value: 'userProvidedKey', + description: + 'Use your own model API key from credentials. Same provider required for both models.', + }, + ], + default: 'gateway', + description: + 'How model calls are routed. Model Gateway lets you freely mix providers. User-provided API key uses your own key from credentials and requires the Model and Agent Model to be from the same provider.', + }, + { + displayName: 'System Prompt', + name: 'systemPrompt', + type: 'string', + typeOptions: { + rows: 4, + }, + default: '', + placeholder: 'e.g. You are a helpful assistant that extracts data from websites', + description: 'Custom system prompt for the agent', + }, + ], + }, { displayName: 'Model Source', name: 'modelSource', @@ -221,7 +509,7 @@ function buildProperties(): INodeProperties[] { noDataExpression: true, displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { lt: 3 } }], operation: ['execute'], }, }, @@ -240,37 +528,7 @@ function buildProperties(): INodeProperties[] { ], default: 'gateway', description: - 'Choose how model calls are routed. Model Gateway lets you mix providers; User-provided API key requires both models from the same provider.', - }, - { - displayName: 'Model Info', - name: 'modelNoticeGateway', - type: 'notice', - default: '', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - modelSource: ['gateway'], - }, - }, - description: - 'Using the Browserbase Model Gateway. You can freely mix models from different providers for Driver and Agent.', - }, - { - displayName: 'Model Info', - name: 'modelNoticeBYOK', - type: 'notice', - default: '', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - modelSource: ['userProvidedKey'], - }, - }, - description: - 'Using your own API key from credentials. Both Driver and Agent models MUST be from the same provider.', + 'How model calls are routed. Model Gateway lets you freely mix providers for Driver and Agent. User-provided API key uses your own key from credentials and requires both models from the same provider.', }, { displayName: 'Driver Model', @@ -278,7 +536,7 @@ function buildProperties(): INodeProperties[] { type: 'options', displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { lt: 3 } }], operation: ['execute'], }, }, @@ -297,11 +555,11 @@ function buildProperties(): INodeProperties[] { }, { name: 'Gemini 3 Flash (Google)', - value: 'google/gemini-3-flash', + value: 'google/gemini-3-flash-preview', }, { name: 'Gemini 3 Pro (Google)', - value: 'google/gemini-3-pro', + value: 'google/gemini-3-pro-preview', }, { name: 'GPT-4o (OpenAI)', @@ -313,7 +571,8 @@ function buildProperties(): INodeProperties[] { }, ], default: 'anthropic/claude-sonnet-4-6', - description: 'Model for browser session (DOM-based, used for navigation)', + description: + 'Model for browser session (DOM-based, used for navigation). See Stagehand model evals to compare model performance.', }, { displayName: 'Mode', @@ -321,7 +580,7 @@ function buildProperties(): INodeProperties[] { type: 'options', displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { lt: 3 } }], operation: ['execute'], }, }, @@ -343,7 +602,8 @@ function buildProperties(): INodeProperties[] { }, ], default: 'cua', - description: 'Agent mode determines how the agent interacts with pages', + description: + 'How the agent interacts with pages. CUA uses vision/coordinates (best for complex UIs). DOM uses selectors (faster, works with any LLM). Hybrid combines both. How to pick a mode.', }, { displayName: 'Agent Model', @@ -351,47 +611,15 @@ function buildProperties(): INodeProperties[] { type: 'options', displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { lt: 3 } }], operation: ['execute'], mode: ['cua'], }, }, - options: [ - { - name: 'Claude Haiku 4.5 (Anthropic)', - value: 'anthropic/claude-haiku-4-5', - }, - { - name: 'Claude Opus 4.6 (Anthropic)', - value: 'anthropic/claude-opus-4-6', - }, - { - name: 'Claude Sonnet 4.6 (Anthropic)', - value: 'anthropic/claude-sonnet-4-6', - }, - { - name: 'Computer Use Preview (2025-03-11, OpenAI)', - value: 'openai/computer-use-preview-2025-03-11', - }, - { - name: 'Computer Use Preview (OpenAI)', - value: 'openai/computer-use-preview', - }, - { - name: 'Gemini 2.5 CUA (Google)', - value: 'google/gemini-2.5-computer-use-preview-10-2025', - }, - { - name: 'Gemini 3 Flash (Google)', - value: 'google/gemini-3-flash-preview', - }, - { - name: 'Gemini 3 Pro (Google)', - value: 'google/gemini-3-pro-preview', - }, - ], + options: CUA_MODEL_OPTIONS, default: 'anthropic/claude-sonnet-4-6', - description: 'CUA model for vision-based browser control', + description: + 'CUA model for vision-based browser control. See Stagehand model evals to compare model performance.', }, { displayName: 'Agent Model', @@ -399,7 +627,7 @@ function buildProperties(): INodeProperties[] { type: 'options', displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { lt: 3 } }], operation: ['execute'], mode: ['dom'], }, @@ -431,7 +659,8 @@ function buildProperties(): INodeProperties[] { }, ], default: 'anthropic/claude-sonnet-4-6', - description: 'LLM for DOM-based browser control', + description: + 'LLM for DOM-based browser control. See Stagehand model evals to compare model performance.', }, { displayName: 'Agent Model', @@ -439,27 +668,15 @@ function buildProperties(): INodeProperties[] { type: 'options', displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { lt: 3 } }], operation: ['execute'], mode: ['hybrid'], }, }, - options: [ - { - name: 'Gemini 3 Flash (Google)', - value: 'google/gemini-3-flash-preview', - }, - { - name: 'Claude Sonnet 4.6 (Anthropic)', - value: 'anthropic/claude-sonnet-4-6', - }, - { - name: 'Claude Haiku 4.5 (Anthropic)', - value: 'anthropic/claude-haiku-4-5-20251001', - }, - ], + options: HYBRID_MODEL_OPTIONS, default: 'anthropic/claude-sonnet-4-6', - description: 'Model for hybrid mode (must support coordinate actions)', + description: + 'Model for hybrid mode (must support coordinate actions). See Stagehand model evals to compare model performance.', }, { displayName: 'Options', @@ -469,7 +686,7 @@ function buildProperties(): INodeProperties[] { default: {}, displayOptions: { show: { - resource: ['agent'], + '@version': [{ _cnd: { lt: 3 } }], operation: ['execute'], }, }, @@ -512,9 +729,7 @@ function buildProperties(): INodeProperties[] { 'Pass sensitive data to the agent. The LLM sees %variableName% placeholders and descriptions, but never the actual values.', displayOptions: { show: { - resource: ['agent'], operation: ['execute'], - mode: ['dom', 'hybrid'], }, }, options: [ @@ -560,7 +775,6 @@ function buildProperties(): INodeProperties[] { default: {}, displayOptions: { show: { - resource: ['agent'], operation: ['execute'], }, }, @@ -625,7 +839,6 @@ function buildProperties(): INodeProperties[] { default: {}, displayOptions: { show: { - resource: ['agent'], operation: ['execute'], }, }, @@ -712,7 +925,6 @@ function buildProperties(): INodeProperties[] { description: 'The search query to run', displayOptions: { show: { - resource: ['search'], operation: ['search'], }, }, @@ -729,7 +941,6 @@ function buildProperties(): INodeProperties[] { description: 'How many search results to return (1-25)', displayOptions: { show: { - resource: ['search'], operation: ['search'], }, }, @@ -744,7 +955,6 @@ function buildProperties(): INodeProperties[] { description: 'The URL to fetch', displayOptions: { show: { - resource: ['fetch'], operation: ['fetch'], }, }, @@ -757,7 +967,6 @@ function buildProperties(): INodeProperties[] { default: {}, displayOptions: { show: { - resource: ['fetch'], operation: ['fetch'], }, }, @@ -794,9 +1003,9 @@ export class Browserbase implements INodeType { name: 'browserbase', icon: 'file:../../icons/browserbase.svg', group: ['transform'], - version: [2, 2.1], + version: [2, 2.1, 3], subtitle: - '={{$parameter["resource"] === "agent" ? $parameter["operation"] + ": " + $parameter["mode"] : $parameter["operation"]}}', + '={{$parameter["operation"] === "execute" ? "Run an agent" : ($parameter["operation"] === "fetch" ? "Fetch a webpage" : "Search the web")}}', description: 'Browser automation, web search, and page fetches with Browserbase.', defaults: { name: 'Browserbase', @@ -959,17 +1168,69 @@ export class Browserbase implements INodeType { url = normalizeUrl(url); const instruction = executeFunctions.getNodeParameter('instruction', itemIndex) as string; - const modelSource = executeFunctions.getNodeParameter('modelSource', itemIndex) as string; const driverModel = executeFunctions.getNodeParameter('driverModel', itemIndex) as string; - const mode = executeFunctions.getNodeParameter('mode', itemIndex) as string; + let mode: string; + let modelSource: string; let agentModel: string; - if (mode === 'cua') { - agentModel = executeFunctions.getNodeParameter('modelCua', itemIndex) as string; - } else if (mode === 'dom') { - agentModel = executeFunctions.getNodeParameter('modelDom', itemIndex) as string; + let maxSteps: number; + let systemPrompt: string | undefined; + let highlightCursor: boolean; + + if (executeFunctions.getNode().typeVersion >= 3) { + // v3+: model settings live in the "Model Options" collection. + const modelOptions = executeFunctions.getNodeParameter('modelOptions', itemIndex, {}) as { + mode?: string; + modelSource?: string; + modelCua?: string; + modelDom?: string; + modelHybrid?: string; + maxSteps?: number; + systemPrompt?: string; + highlightCursor?: boolean; + }; + mode = modelOptions.mode ?? 'cua'; + modelSource = modelOptions.modelSource ?? 'gateway'; + // Agent Model is an optional override; when unset it falls back to the top-level Model. + if (mode === 'cua') { + agentModel = modelOptions.modelCua || driverModel; + } else if (mode === 'dom') { + agentModel = modelOptions.modelDom || driverModel; + } else { + agentModel = modelOptions.modelHybrid || driverModel; + } + maxSteps = modelOptions.maxSteps ?? 20; + systemPrompt = modelOptions.systemPrompt; + highlightCursor = modelOptions.highlightCursor ?? true; } else { - agentModel = executeFunctions.getNodeParameter('modelHybrid', itemIndex) as string; + // v2/2.1: legacy top-level model fields and "Options" collection. + modelSource = executeFunctions.getNodeParameter('modelSource', itemIndex, 'gateway') as string; + mode = executeFunctions.getNodeParameter('mode', itemIndex, 'cua') as string; + if (mode === 'cua') { + agentModel = executeFunctions.getNodeParameter('modelCua', itemIndex) as string; + } else if (mode === 'dom') { + agentModel = executeFunctions.getNodeParameter('modelDom', itemIndex) as string; + } else { + agentModel = executeFunctions.getNodeParameter('modelHybrid', itemIndex) as string; + } + const options = executeFunctions.getNodeParameter('options', itemIndex, {}) as { + maxSteps?: number; + systemPrompt?: string; + highlightCursor?: boolean; + }; + maxSteps = options.maxSteps ?? 20; + systemPrompt = options.systemPrompt; + highlightCursor = options.highlightCursor ?? true; + } + + // CUA mode only works with computer-use-capable models. When the Agent + // Model override is unset it falls back to the driver Model, which may not + // be a CUA model — sending it would make Stagehand silently skip CUA mode. + if (mode === 'cua' && !CUA_MODELS.has(agentModel)) { + throw new NodeOperationError( + executeFunctions.getNode(), + `CUA mode requires a computer-use-capable Agent Model, but "${agentModel}" is not one. Set "Agent Model" in Model Options to a CUA model (e.g. google/gemini-3-flash-preview), or switch Mode to DOM.`, + ); } if (modelSource === 'userProvidedKey') { @@ -978,16 +1239,11 @@ export class Browserbase implements INodeType { if (driverProvider !== agentProvider) { throw new NodeOperationError( executeFunctions.getNode(), - `When using your own model API key, both Driver and Agent models must be from the same provider. Driver is "${driverProvider}", Agent is "${agentProvider}".`, + `When using your own model API key, the Model and Agent Model must be from the same provider. Model is "${driverProvider}", Agent Model is "${agentProvider}".`, ); } } - const options = executeFunctions.getNodeParameter('options', itemIndex, {}) as { - maxSteps?: number; - systemPrompt?: string; - highlightCursor?: boolean; - }; const browserOptions = executeFunctions.getNodeParameter( 'browserOptions', itemIndex, @@ -1091,17 +1347,17 @@ export class Browserbase implements INodeType { model: agentModel, }; - if (options.systemPrompt) { - agentConfigBody.systemPrompt = options.systemPrompt; + if (systemPrompt) { + agentConfigBody.systemPrompt = systemPrompt; } const executeOptions: Record = { instruction, - maxSteps: options.maxSteps ?? 20, + maxSteps, }; - if ((mode === 'cua' || mode === 'hybrid') && options.highlightCursor !== false) { - executeOptions.highlightCursor = options.highlightCursor ?? true; + if ((mode === 'cua' || mode === 'hybrid') && highlightCursor !== false) { + executeOptions.highlightCursor = highlightCursor; } if (mode === 'dom' || mode === 'hybrid') { @@ -1192,20 +1448,33 @@ export class Browserbase implements INodeType { for (let i = 0; i < items.length; i++) { try { - const resource = this.getNodeParameter('resource', i) as string; - const modelSource = this.getNodeParameter('modelSource', i, 'gateway') as string; + const operation = this.getNodeParameter('operation', i) as string; + let modelSource = 'gateway'; + if (operation === 'execute') { + if (this.getNode().typeVersion >= 3) { + const itemModelOptions = this.getNodeParameter('modelOptions', i, {}) as { + modelSource?: string; + }; + modelSource = itemModelOptions.modelSource ?? 'gateway'; + } else { + modelSource = this.getNodeParameter('modelSource', i, 'gateway') as string; + } + } const credentials = await this.getCredentials('browserbaseApi'); - if (resource === 'agent' && modelSource === 'userProvidedKey' && !credentials.modelApiKey) { - throw new NodeOperationError( - this.getNode(), - 'Model Source is set to "User-provided API key" but no Model API Key is configured in the Browserbase credentials.', - ); + let modelApiKey: string | undefined; + if (operation === 'execute' && modelSource === 'userProvidedKey') { + const provider = (this.getNodeParameter('driverModel', i) as string).split('/')[0]; + modelApiKey = resolveModelApiKey(credentials, provider); + if (!modelApiKey) { + throw new NodeOperationError( + this.getNode(), + `Model Source is set to "User-provided API key" but no API key for "${provider}" is configured in the Browserbase credentials.`, + ); + } } - const headers = getHeaders(credentials, { - includeModelApiKey: resource === 'agent' && modelSource === 'userProvidedKey', - }); + const headers = getHeaders(credentials, { modelApiKey }); const useCredentialBaseUrls = this.getNode().typeVersion >= 2.1; const apiBaseUrl = useCredentialBaseUrls @@ -1215,9 +1484,9 @@ export class Browserbase implements INodeType { ? normalizeBaseUrl((credentials.stagehandBaseUrl as string) || STAGEHAND_BASE_URL) : STAGEHAND_BASE_URL; - if (resource === 'search') { + if (operation === 'search') { returnData.push(await node.executeSearch(this, i, headers, apiBaseUrl)); - } else if (resource === 'fetch') { + } else if (operation === 'fetch') { returnData.push(await node.executeFetch(this, i, headers, apiBaseUrl)); } else { returnData.push(await node.executeAgent(this, i, headers, stagehandBaseUrl));