Skip to content

Commit 8eccd56

Browse files
committed
send client options on every request
1 parent 04fb315 commit 8eccd56

File tree

13 files changed

+1312
-129
lines changed

13 files changed

+1312
-129
lines changed

CHANGELOG.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,13 @@
233233
We're thrilled to announce the release of Stagehand 2.0, bringing significant improvements to make browser automation more powerful, faster, and easier to use than ever before.
234234

235235
### 🚀 New Features
236-
237236
- **Introducing `stagehand.agent`**: A powerful new way to integrate SOTA Computer use models or Browserbase's [Open Operator](https://operator.browserbase.com) into Stagehand with one line of code! Perfect for multi-step workflows and complex interactions. [Learn more](https://docs.stagehand.dev/concepts/agent)
238237
- **Lightning-fast `act` and `extract`**: Major performance improvements to make your automations run significantly faster.
239238
- **Enhanced Logging**: Better visibility into what's happening during automation with improved logging and debugging capabilities.
240239
- **Comprehensive Documentation**: A completely revamped documentation site with better examples, guides, and best practices.
241240
- **Improved Error Handling**: More descriptive errors and better error recovery to help you debug issues faster.
242241

243242
### 🛠️ Developer Experience
244-
245243
- **Better TypeScript Support**: Enhanced type definitions and better IDE integration
246244
- **Better Error Messages**: Clearer, more actionable error messages to help you debug faster
247245
- **Improved Caching**: More reliable action caching for better performance
@@ -502,7 +500,6 @@
502500
- [#316](https://github.com/browserbase/stagehand/pull/316) [`902e633`](https://github.com/browserbase/stagehand/commit/902e633e126a58b80b757ea0ecada01a7675a473) Thanks [@kamath](https://github.com/kamath)! - rename browserbaseResumeSessionID -> browserbaseSessionID
503501

504502
- [#296](https://github.com/browserbase/stagehand/pull/296) [`f11da27`](https://github.com/browserbase/stagehand/commit/f11da27a20409c240ceeea2003d520f676def61a) Thanks [@kamath](https://github.com/kamath)! - - Deprecate fields in `init` in favor of constructor options
505-
506503
- Deprecate `initFromPage` in favor of `browserbaseResumeSessionID` in constructor
507504
- Rename `browserBaseSessionCreateParams` -> `browserbaseSessionCreateParams`
508505

lib/StagehandPage.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,7 @@ ${scriptContent} \
739739
const result = await this.api.act({
740740
...observeResult,
741741
frameId: this.rootFrameId,
742+
modelClientOptions: this.stagehand["modelClientOptions"],
742743
});
743744
this.stagehand.addToHistory("act", observeResult, result);
744745
return result;
@@ -836,7 +837,10 @@ ${scriptContent} \
836837
if (!instructionOrOptions) {
837838
let result: ExtractResult<T>;
838839
if (this.api) {
839-
result = await this.api.extract<T>({ frameId: this.rootFrameId });
840+
result = await this.api.extract<T>({
841+
frameId: this.rootFrameId,
842+
modelClientOptions: this.stagehand["modelClientOptions"],
843+
});
840844
} else {
841845
result = await this.extractHandler.extract();
842846
}

lib/a11y/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ export async function buildBackendIdMaps(
183183
if (n.contentDocument && locate(n.contentDocument)) return true;
184184
return false;
185185
} else {
186-
if (n.backendNodeId === backendNodeId) return (iframeNode = n), true;
186+
if (n.backendNodeId === backendNodeId)
187+
return ((iframeNode = n), true);
187188
return (
188189
(n.children?.some(locate) ?? false) ||
189190
(n.contentDocument ? locate(n.contentDocument) : false)

lib/api.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export class StagehandAPI {
4848

4949
async init({
5050
modelName,
51-
modelApiKey,
5251
domSettleTimeoutMs,
5352
verbose,
5453
debugDom,
@@ -59,11 +58,6 @@ export class StagehandAPI {
5958
browserbaseSessionCreateParams,
6059
browserbaseSessionID,
6160
}: StartSessionParams): Promise<StartSessionResult> {
62-
if (!modelApiKey) {
63-
throw new StagehandAPIError("modelApiKey is required");
64-
}
65-
this.modelApiKey = modelApiKey;
66-
6761
const region = browserbaseSessionCreateParams?.region;
6862
if (region && region !== "us-west-2") {
6963
return { sessionId: browserbaseSessionID ?? null, available: false };
@@ -186,10 +180,19 @@ export class StagehandAPI {
186180
const queryString = urlParams.toString();
187181
const url = `/sessions/${this.sessionId}/${method}${queryString ? `?${queryString}` : ""}`;
188182

189-
const response = await this.request(url, {
190-
method: "POST",
191-
body: JSON.stringify(args),
192-
});
183+
// Extract modelClientOptions from args if present
184+
const modelClientOptions = (
185+
args as { modelClientOptions?: Record<string, unknown> }
186+
)?.modelClientOptions;
187+
188+
const response = await this.request(
189+
url,
190+
{
191+
method: "POST",
192+
body: JSON.stringify(args),
193+
},
194+
modelClientOptions,
195+
);
193196

194197
if (!response.ok) {
195198
const errorBody = await response.text();
@@ -248,6 +251,7 @@ export class StagehandAPI {
248251
private async request(
249252
path: string,
250253
options: RequestInit = {},
254+
modelClientOptions?: Record<string, unknown>,
251255
): Promise<Response> {
252256
const defaultHeaders: Record<string, string> = {
253257
"x-bb-api-key": this.apiKey,
@@ -261,6 +265,12 @@ export class StagehandAPI {
261265
"x-sdk-version": STAGEHAND_VERSION,
262266
};
263267

268+
// Add modelClientOptions as a header if provided
269+
if (modelClientOptions) {
270+
defaultHeaders["x-model-client-options"] =
271+
JSON.stringify(modelClientOptions);
272+
}
273+
264274
if (options.method === "POST" && options.body) {
265275
defaultHeaders["Content-Type"] = "application/json";
266276
}

lib/index.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ import { LLMProvider } from "./llm/LLMProvider";
4545
import { StagehandLogger } from "./logger";
4646
import { connectToMCPServer } from "./mcp/connection";
4747
import { resolveTools } from "./mcp/utils";
48-
import { isRunningInBun, loadApiKeyFromEnv } from "./utils";
48+
import {
49+
isRunningInBun,
50+
loadApiKeyFromEnv,
51+
loadBedrockClientOptions,
52+
} from "./utils";
4953

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

@@ -587,10 +591,23 @@ export class Stagehand {
587591
if (!modelClientOptions?.apiKey) {
588592
// If no API key is provided, try to load it from the environment
589593
if (LLMProvider.getModelProvider(this.modelName) === "aisdk") {
590-
modelApiKey = loadApiKeyFromEnv(
591-
this.modelName.split("/")[0],
592-
this.logger,
593-
);
594+
const provider = this.modelName.split("/")[0];
595+
596+
// Special handling for Amazon Bedrock's complex authentication
597+
if (provider === "bedrock") {
598+
const bedrockOptions = loadBedrockClientOptions(this.logger);
599+
this.modelClientOptions = {
600+
...modelClientOptions,
601+
...bedrockOptions,
602+
};
603+
} else {
604+
// Standard single API key handling for other AISDK providers
605+
modelApiKey = loadApiKeyFromEnv(provider, this.logger);
606+
this.modelClientOptions = {
607+
...modelClientOptions,
608+
apiKey: modelApiKey,
609+
};
610+
}
594611
} else {
595612
// Temporary add for legacy providers
596613
modelApiKey =
@@ -601,11 +618,11 @@ export class Stagehand {
601618
: LLMProvider.getModelProvider(this.modelName) === "google"
602619
? process.env.GOOGLE_API_KEY
603620
: undefined;
621+
this.modelClientOptions = {
622+
...modelClientOptions,
623+
apiKey: modelApiKey,
624+
};
604625
}
605-
this.modelClientOptions = {
606-
...modelClientOptions,
607-
apiKey: modelApiKey,
608-
};
609626
} else {
610627
this.modelClientOptions = modelClientOptions;
611628
}
@@ -753,7 +770,7 @@ export class Stagehand {
753770
logger: this.logger,
754771
});
755772

756-
const modelApiKey = this.modelClientOptions?.apiKey;
773+
const modelApiKey = this.modelClientOptions?.apiKey as string;
757774
const { sessionId, available } = await this.apiClient.init({
758775
modelName: this.modelName,
759776
modelApiKey: modelApiKey,

lib/llm/CerebrasClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class CerebrasClient extends LLMClient {
3131
enableCaching?: boolean;
3232
cache?: LLMCache;
3333
modelName: AvailableModel;
34-
clientOptions?: OpenAI.ClientOptions;
34+
clientOptions?: ClientOptions;
3535
userProvidedInstructions?: string;
3636
}) {
3737
super(modelName, userProvidedInstructions);

lib/llm/GoogleClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class GoogleClient extends LLMClient {
8383
clientOptions.apiKey = loadApiKeyFromEnv("google_legacy", logger);
8484
}
8585
this.clientOptions = clientOptions;
86-
this.client = new GoogleGenAI({ apiKey: clientOptions.apiKey });
86+
this.client = new GoogleGenAI({ apiKey: clientOptions.apiKey as string });
8787
this.cache = cache;
8888
this.enableCaching = enableCaching;
8989
this.modelName = modelName;

lib/llm/LLMProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "@/types/stagehandErrors";
77
import { anthropic, createAnthropic } from "@ai-sdk/anthropic";
88
import { azure, createAzure } from "@ai-sdk/azure";
9+
import { bedrock, createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
910
import { cerebras, createCerebras } from "@ai-sdk/cerebras";
1011
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek";
1112
import { createGoogleGenerativeAI, google } from "@ai-sdk/google";
@@ -38,6 +39,7 @@ const AISDKProviders: Record<string, AISDKProvider> = {
3839
xai,
3940
azure,
4041
groq,
42+
bedrock,
4143
cerebras,
4244
togetherai,
4345
mistral,
@@ -52,6 +54,7 @@ const AISDKProvidersWithAPIKey: Record<string, AISDKCustomProvider> = {
5254
xai: createXai,
5355
azure: createAzure,
5456
groq: createGroq,
57+
bedrock: createAmazonBedrock,
5558
cerebras: createCerebras,
5659
togetherai: createTogetherAI,
5760
mistral: createMistral,
@@ -108,7 +111,7 @@ export function getAISDKLanguageModel(
108111
);
109112
}
110113
// Create the provider instance with the custom configuration options
111-
const provider = creator(modelClientOptions);
114+
const provider = creator(modelClientOptions as Record<string, unknown>);
112115
// Get the specific model from the provider
113116
return provider(subModelName);
114117
} else {

lib/utils.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ export const providerEnvVarMap: Partial<
452452
perplexity: "PERPLEXITY_API_KEY",
453453
azure: "AZURE_API_KEY",
454454
xai: "XAI_API_KEY",
455+
bedrock: "AWS_BEARER_TOKEN_BEDROCK", // Primary API key method for Bedrock
455456
google_legacy: "GOOGLE_API_KEY",
456457
};
457458

@@ -493,6 +494,75 @@ export function loadApiKeyFromEnv(
493494
return undefined;
494495
}
495496

497+
/**
498+
* Loads Amazon Bedrock client configuration from environment variables.
499+
* Supports both API key authentication and SigV4 authentication methods.
500+
* @param logger Logger function for info/error messages
501+
* @returns Bedrock client options object or undefined if no authentication method is available
502+
*/
503+
export function loadBedrockClientOptions(
504+
logger: (logLine: LogLine) => void,
505+
): Record<string, unknown> | undefined {
506+
// Authentication precedence:
507+
// 1. API key from AWS_BEARER_TOKEN_BEDROCK (recommended)
508+
// 2. SigV4 authentication using AWS credentials
509+
510+
const bearerToken = process.env.AWS_BEARER_TOKEN_BEDROCK;
511+
const region =
512+
process.env.AWS_DEFAULT_REGION || process.env.AWS_REGION || "us-east-1"; // Default to us-east-1
513+
514+
// Method 1: API key authentication (recommended)
515+
if (bearerToken && bearerToken.length > 0) {
516+
logger({
517+
category: "init",
518+
message: "Using Amazon Bedrock API key authentication",
519+
level: 1,
520+
});
521+
522+
const config: Record<string, unknown> = {
523+
apiKey: bearerToken,
524+
region: region, // Always include region (defaults to us-east-1)
525+
};
526+
527+
return config;
528+
}
529+
530+
// Method 2: Check for SigV4 authentication credentials
531+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
532+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
533+
const sessionToken = process.env.AWS_SESSION_TOKEN;
534+
535+
if (accessKeyId && secretAccessKey) {
536+
logger({
537+
category: "init",
538+
message: "Using Amazon Bedrock SigV4 authentication",
539+
level: 1,
540+
});
541+
542+
const config: Record<string, unknown> = {
543+
accessKeyId,
544+
secretAccessKey,
545+
region: region, // Always include region (defaults to us-east-1)
546+
};
547+
548+
// Add session token if present (for temporary credentials)
549+
if (sessionToken && sessionToken.length > 0) {
550+
config.sessionToken = sessionToken;
551+
}
552+
553+
return config;
554+
}
555+
556+
logger({
557+
category: "init",
558+
message:
559+
"No Amazon Bedrock authentication credentials found. Please set either AWS_BEARER_TOKEN_BEDROCK for API key auth or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (and optionally AWS_SESSION_TOKEN) for SigV4 auth",
560+
level: 0,
561+
});
562+
563+
return undefined;
564+
}
565+
496566
export function trimTrailingTextNode(
497567
path: string | undefined,
498568
): string | undefined {

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,10 @@
8383
"pino-pretty": "^13.0.0",
8484
"playwright": "^1.52.0",
8585
"ws": "^8.18.0",
86-
"zod-to-json-schema": "^3.23.5"
87-
},
88-
"optionalDependencies": {
86+
"zod-to-json-schema": "^3.23.5",
8987
"@ai-sdk/anthropic": "^1.2.6",
9088
"@ai-sdk/azure": "^1.3.19",
89+
"@ai-sdk/amazon-bedrock": "^1.0.0",
9190
"@ai-sdk/cerebras": "^0.2.6",
9291
"@ai-sdk/deepseek": "^0.2.13",
9392
"@ai-sdk/google": "^1.2.6",

0 commit comments

Comments
 (0)