Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
76cc39b
allow free modelclientoptions
sameelarif Aug 29, 2025
28f4b6e
Merge branch 'main' into sameel/stg-692-azurebedrock-api-integration-…
sameelarif Sep 3, 2025
1a415bb
Merge branch 'main' into sameel/stg-692-azurebedrock-api-integration-…
sameelarif Sep 9, 2025
04fb315
change zod ver to working build
sameelarif Sep 10, 2025
8eccd56
send client options on every request
sameelarif Sep 15, 2025
c6a752d
test bedrock file
filip-michalsky Sep 23, 2025
2931804
add azure test file
filip-michalsky Sep 23, 2025
2f3b8b9
fix bedrock test
sameelarif Sep 24, 2025
be8b7a4
Merge branch 'main' into sameel/stg-692-azurebedrock-api-integration-…
sameelarif Sep 25, 2025
27c722c
Update pnpm-lock.yaml
sameelarif Sep 25, 2025
69c3d93
better modelclientoption api handling
sameelarif Sep 26, 2025
467dade
dont override region
sameelarif Sep 26, 2025
0735ca3
fix bedrock example
sameelarif Sep 26, 2025
76b44ae
lint
sameelarif Sep 30, 2025
18937ee
read aws creds from client options obj
sameelarif Sep 30, 2025
0af4acf
update evals cli docs (#1096)
miguelg719 Sep 26, 2025
c762944
adding support for new claude 4.5 sonnet agent model (#1099)
Kylejeong2 Sep 29, 2025
4bd7412
properly convert custom / mcp tools to anthropic cua format (#1103)
tkattkat Oct 1, 2025
ce07cfa
Add current date and page url to agent context (#1102)
miguelg719 Oct 1, 2025
06ae0e6
Additional agent logging (#1104)
miguelg719 Oct 1, 2025
9fe40fd
fix system prompt
miguelg719 Oct 2, 2025
938b51c
remove dup log
miguelg719 Oct 2, 2025
607b4c3
pass modelClientOptions for stagehand agent
miguelg719 Oct 2, 2025
adec13c
Merge branch 'main' into sameel/stg-692-azurebedrock-api-integration-…
sameelarif Oct 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions examples/example-bedrock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod/v3";

/**
* AWS Bedrock Integration Example for Stagehand
*
* This example demonstrates how to use Stagehand with AWS Bedrock models.
* Anthropic models work best for advanced features like structured extraction
* and observation, while OpenAI models work well for navigation and basic operations.
*
* SETUP:
*
* 1. Enable model access in AWS Bedrock Console:
* - Visit: https://console.aws.amazon.com/bedrock/
* - Go to "Model access" → "Enable model access"
* - Enable desired models (e.g., Anthropic Claude, OpenAI models)
* - Wait for approval
*
* 2. Authentication (choose one):
*
* Option A - Bearer Token:
* ```
* AWS_BEARER_TOKEN_BEDROCK=bedrock-api-key-[your-base64-token]
* ```
*
* Option B - Standard AWS Credentials:
* ```
* AWS_ACCESS_KEY_ID=your-access-key
* AWS_SECRET_ACCESS_KEY=your-secret-key
* ```
*
* 3. Set region and model:
* ```
* AWS_REGION=us-east-1
* ```
*
* RECOMMENDED MODELS:
* - anthropic.claude-3-5-sonnet-20240620-v1:0 (best for extraction/observation)
* - anthropic.claude-3-haiku-20240307-v1:0 (faster, good for basic tasks)
* - openai.gpt-oss-120b-1:0 (good for navigation and simple operations)
*/

async function runBedrockExample() {
// Initialize Stagehand with AWS Bedrock
const stagehand = new Stagehand({
env: "BROWSERBASE",
modelName: "bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0",
modelClientOptions: {
region: "us-west-2", // Will use environment variables if not specified
},
});

try {
await stagehand.init();
const page = stagehand.page;

console.log("🚀 Stagehand initialized with AWS Bedrock");

// Navigate to a website
await page.goto("https://example.com");
console.log("📄 Navigated to example.com");

// Perform actions on the page
await page.act("click the link");
console.log("🎯 Clicked the 'More information...' link");

// Extract structured data
const pageInfo = await page.extract({
instruction: "Extract the page title and text",
schema: z.object({
title: z.string(),
text: z.string(),
}),
});

console.log("📊 Extracted data:", pageInfo);

// Observe elements on the page
const elements = await page.observe();
console.log(`👀 Found ${elements.length} interactive elements`);

console.log("✅ AWS Bedrock example completed successfully!");
} catch (error) {
console.error("❌ Error:", error);

// Common troubleshooting hints
if (error.message?.includes("access")) {
console.error("💡 Check model access in AWS Bedrock Console");
} else if (
error.message?.includes("credentials") ||
error.message?.includes("authentication")
) {
console.error("💡 Verify your AWS credentials are set correctly");
}
} finally {
await stagehand.close();
}
}

// Run the example
runBedrockExample();
9 changes: 8 additions & 1 deletion lib/StagehandPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ ${scriptContent} \
const result = await this.api.act({
...observeResult,
frameId: this.rootFrameId,
modelClientOptions: this.stagehand["modelClientOptions"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need this once we add self-healing

});
this.stagehand.addToHistory("act", observeResult, result);
return result;
Expand Down Expand Up @@ -783,6 +784,7 @@ ${scriptContent} \
frameId: this.rootFrameId,
modelClientOptions:
modelClientOptions || this.stagehand["modelClientOptions"],
modelName: modelName || this.stagehand["modelName"],
};
const result = await this.api.act(opts);
this.stagehand.addToHistory("act", actionOrOptions, result);
Expand Down Expand Up @@ -844,7 +846,10 @@ ${scriptContent} \
if (!instructionOrOptions) {
let result: ExtractResult<T>;
if (this.api) {
result = await this.api.extract<T>({ frameId: this.rootFrameId });
result = await this.api.extract<T>({
frameId: this.rootFrameId,
modelClientOptions: this.stagehand["modelClientOptions"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes because otherwise it doesn't get sent to the API. We need this param on all api calls now

});
} else {
result = await this.extractHandler.extract();
}
Expand Down Expand Up @@ -882,6 +887,7 @@ ${scriptContent} \
frameId: this.rootFrameId,
modelClientOptions:
modelClientOptions || this.stagehand["modelClientOptions"],
modelName: modelName || this.stagehand["modelName"],
};
const result = await this.api.extract<T>(opts);
this.stagehand.addToHistory("extract", instructionOrOptions, result);
Expand Down Expand Up @@ -991,6 +997,7 @@ ${scriptContent} \
frameId: this.rootFrameId,
modelClientOptions:
modelClientOptions || this.stagehand["modelClientOptions"],
modelName: modelName || this.stagehand["modelName"],
};
const result = await this.api.observe(opts);
this.stagehand.addToHistory("observe", instructionOrOptions, result);
Expand Down
12 changes: 6 additions & 6 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class StagehandAPI {

async init({
modelName,
modelApiKey,
domSettleTimeoutMs,
verbose,
debugDom,
Expand All @@ -59,11 +58,6 @@ export class StagehandAPI {
browserbaseSessionCreateParams,
browserbaseSessionID,
}: StartSessionParams): Promise<StartSessionResult> {
if (!modelApiKey) {
throw new StagehandAPIError("modelApiKey is required");
}
this.modelApiKey = modelApiKey;

const region = browserbaseSessionCreateParams?.region;
if (region && region !== "us-west-2") {
return { sessionId: browserbaseSessionID ?? null, available: false };
Expand Down Expand Up @@ -191,6 +185,12 @@ export class StagehandAPI {
const queryString = urlParams.toString();
const url = `/sessions/${this.sessionId}/${method}${queryString ? `?${queryString}` : ""}`;

this.logger({
category: "execute",
message: `Executing ${method} with args: ${JSON.stringify(args)}`,
level: 2,
});

const response = await this.request(url, {
method: "POST",
body: JSON.stringify(args),
Expand Down
12 changes: 5 additions & 7 deletions lib/handlers/stagehandAgentHandler.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import {
ActToolResult,
AgentAction,
AgentExecuteOptions,
AgentResult,
ActToolResult,
} from "@/types/agent";
import { LogLine } from "@/types/log";
import { LLMClient } from "../llm/LLMClient";
import { CoreMessage, wrapLanguageModel } from "ai";
import { LanguageModel } from "ai";
import { processMessages } from "../agent/utils/messageProcessing";
import { CoreMessage, LanguageModel, ToolSet, wrapLanguageModel } from "ai";
import { createAgentTools } from "../agent/tools";
import { ToolSet } from "ai";
import { processMessages } from "../agent/utils/messageProcessing";
import { Stagehand } from "../index";
import { LLMClient } from "../llm/LLMClient";

export class StagehandAgentHandler {
private stagehand: Stagehand;
Expand Down Expand Up @@ -280,7 +278,7 @@ STRATEGY:
- Keep actions atomic and verify outcomes before proceeding.

For each action, provide clear reasoning about why you're taking that step.
Today's date is ${new Date().toLocaleDateString()}. You're currently on the website: ${this.stagehand.page.url}.`;
Today's date is ${new Date().toLocaleDateString()}. You're currently on the website: ${this.stagehand.page.url()}.`;
}

private createTools() {
Expand Down
66 changes: 44 additions & 22 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ import { StagehandAgentHandler } from "./handlers/stagehandAgentHandler";
import { StagehandLogger } from "./logger";
import { connectToMCPServer } from "./mcp/connection";
import { resolveTools } from "./mcp/utils";
import { isRunningInBun, loadApiKeyFromEnv } from "./utils";
import {
isRunningInBun,
loadApiKeyFromEnv,
loadBedrockClientOptions,
} from "./utils";

dotenv.config({ path: ".env" });

Expand Down Expand Up @@ -588,28 +592,41 @@ export class Stagehand {
if (!modelClientOptions?.apiKey) {
// If no API key is provided, try to load it from the environment
if (LLMProvider.getModelProvider(this.modelName) === "aisdk") {
modelApiKey = loadApiKeyFromEnv(
this.modelName.split("/")[0],
this.logger,
);
const provider = this.modelName.split("/")[0];

// Special handling for Amazon Bedrock's complex authentication
if (provider === "bedrock") {
const bedrockOptions = loadBedrockClientOptions(
this.logger,
modelClientOptions,
);
this.modelClientOptions = {
...modelClientOptions,
...bedrockOptions,
};
} else {
// Standard single API key handling for other AISDK providers
modelApiKey = loadApiKeyFromEnv(provider, this.logger);
this.modelClientOptions = {
...modelClientOptions,
apiKey: modelApiKey,
};
}
} else {
// Temporary add for legacy providers
modelApiKey =
LLMProvider.getModelProvider(this.modelName) === "openai"
? process.env.OPENAI_API_KEY ||
this.llmClient?.clientOptions?.apiKey
? process.env.OPENAI_API_KEY
: LLMProvider.getModelProvider(this.modelName) === "anthropic"
? process.env.ANTHROPIC_API_KEY ||
this.llmClient?.clientOptions?.apiKey
? process.env.ANTHROPIC_API_KEY
: LLMProvider.getModelProvider(this.modelName) === "google"
? process.env.GOOGLE_API_KEY ||
this.llmClient?.clientOptions?.apiKey
? process.env.GOOGLE_API_KEY
: undefined;
this.modelClientOptions = {
...modelClientOptions,
apiKey: modelApiKey,
};
}
this.modelClientOptions = {
...modelClientOptions,
apiKey: modelApiKey,
};
} else {
this.modelClientOptions = modelClientOptions;
}
Expand Down Expand Up @@ -757,7 +774,7 @@ export class Stagehand {
logger: this.logger,
});

const modelApiKey = this.modelClientOptions?.apiKey;
const modelApiKey = this.modelClientOptions?.apiKey as string;
const { sessionId, available } = await this.apiClient.init({
modelName: this.modelName,
modelApiKey: modelApiKey,
Expand Down Expand Up @@ -914,11 +931,13 @@ export class Stagehand {
) => Promise<AgentResult>;
setScreenshotCollector?: (collector: unknown) => void;
} {
this.log({
category: "agent",
message: "Creating agent instance",
level: 1,
});
if (!this.usingAPI) {
this.log({
category: "agent",
message: "Creating agent instance",
level: 1,
});
}
let agentHandler: StagehandAgentHandler | CuaAgentHandler | undefined;
if (options?.integrations && !this.experimental) {
throw new StagehandError(
Expand Down Expand Up @@ -989,7 +1008,10 @@ export class Stagehand {
}

if (!options.options) {
options.options = {};
options.options = this.modelClientOptions as Record<
string,
unknown
>;
}
if (options.provider) {
if (options.provider === "anthropic") {
Expand Down
4 changes: 2 additions & 2 deletions lib/llm/AnthropicClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CreateChatCompletionResponseError } from "@/types/stagehandErrors";
import Anthropic, { ClientOptions } from "@anthropic-ai/sdk";
import {
ImageBlockParam,
Expand All @@ -14,14 +15,13 @@ import {
LLMClient,
LLMResponse,
} from "./LLMClient";
import { CreateChatCompletionResponseError } from "@/types/stagehandErrors";

export class AnthropicClient extends LLMClient {
public type = "anthropic" as const;
private client: Anthropic;
private cache: LLMCache | undefined;
private enableCaching: boolean;
public clientOptions: ClientOptions;
public clientOptions?: ClientOptions;

constructor({
enableCaching = false,
Expand Down
7 changes: 3 additions & 4 deletions lib/llm/CerebrasClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import OpenAI from "openai";
import type { ClientOptions } from "openai";
import { CreateChatCompletionResponseError } from "@/types/stagehandErrors";
import OpenAI, { type ClientOptions } from "openai";
import { zodToJsonSchema } from "zod-to-json-schema";
import { LogLine } from "../../types/log";
import { AvailableModel } from "../../types/model";
Expand All @@ -10,14 +10,13 @@ import {
LLMClient,
LLMResponse,
} from "./LLMClient";
import { CreateChatCompletionResponseError } from "@/types/stagehandErrors";

export class CerebrasClient extends LLMClient {
public type = "cerebras" as const;
private client: OpenAI;
private cache: LLMCache | undefined;
private enableCaching: boolean;
public clientOptions: ClientOptions;
public clientOptions?: ClientOptions;
public hasVision = false;

constructor({
Expand Down
2 changes: 1 addition & 1 deletion lib/llm/GoogleClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class GoogleClient extends LLMClient {
clientOptions.apiKey = loadApiKeyFromEnv("google_legacy", logger);
}
this.clientOptions = clientOptions;
this.client = new GoogleGenAI({ apiKey: clientOptions.apiKey });
this.client = new GoogleGenAI({ apiKey: clientOptions.apiKey as string });
this.cache = cache;
this.enableCaching = enableCaching;
this.modelName = modelName;
Expand Down
3 changes: 1 addition & 2 deletions lib/llm/LLMClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "ai";
import { ZodType } from "zod/v3";
import { LogLine } from "../../types/log";
import { AvailableModel, ClientOptions } from "../../types/model";
import { AvailableModel } from "../../types/model";

export interface ChatMessage {
role: "system" | "user" | "assistant";
Expand Down Expand Up @@ -101,7 +101,6 @@ export abstract class LLMClient {
public type: "openai" | "anthropic" | "cerebras" | "groq" | (string & {});
public modelName: AvailableModel | (string & {});
public hasVision: boolean;
public clientOptions: ClientOptions;
public userProvidedInstructions?: string;

constructor(modelName: AvailableModel, userProvidedInstructions?: string) {
Expand Down
Loading