feat: Allow user-provided providerOptions in agent config#1525
feat: Allow user-provided providerOptions in agent config#1525hoverlover wants to merge 1 commit intobrowserbase:mainfrom
Conversation
Add support for users to pass custom providerOptions (e.g., thinkingConfig for Gemini's reasoning output) to the agent via AgentModelConfig. Changes: - Update V3AgentHandler constructor to accept userProviderOptions - Add buildProviderOptions() method to merge user options with defaults - Add deepMerge() helper for nested object merging - Update v3.ts to extract and pass providerOptions from model config - Add comprehensive test suite for providerOptions functionality For Gemini 3 models, the default mediaResolution setting is preserved and merged with any user-provided options. User options take precedence in case of conflicts. Closes browserbase#1524 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Greptile OverviewGreptile SummaryThis PR successfully adds support for user-provided Key Changes:
Architecture: Concerns:
Testing: Confidence Score: 4/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant User
participant V3
participant V3AgentHandler
participant LLMClient
participant AISDK as AI SDK (generateText/streamText)
User->>V3: agent({ model: { modelName, providerOptions } })
V3->>V3: Extract providerOptions from model config
V3->>V3AgentHandler: new V3AgentHandler(..., userProviderOptions)
V3AgentHandler->>V3AgentHandler: Store userProviderOptions
V3-->>User: Return agent instance
User->>V3: agent.execute(instruction)
V3->>V3AgentHandler: execute(options)
V3AgentHandler->>V3AgentHandler: prepareAgent()
V3AgentHandler->>V3AgentHandler: buildProviderOptions(modelId)
alt Is Gemini 3 Model
V3AgentHandler->>V3AgentHandler: Create default { google: { mediaResolution: "MEDIA_RESOLUTION_HIGH" } }
alt Has userProviderOptions
V3AgentHandler->>V3AgentHandler: deepMerge(defaults, userProviderOptions)
Note over V3AgentHandler: User options override defaults
end
else Non-Gemini 3 Model
alt Has userProviderOptions
V3AgentHandler->>V3AgentHandler: Return userProviderOptions as-is
else No userProviderOptions
V3AgentHandler->>V3AgentHandler: Return undefined
end
end
V3AgentHandler->>LLMClient: generateText({ providerOptions, ... })
LLMClient->>AISDK: Call AI SDK with merged providerOptions
AISDK-->>LLMClient: Return result
LLMClient-->>V3AgentHandler: Return result
V3AgentHandler-->>V3: Return AgentResult
V3-->>User: Return result
|
| // Extract user-provided providerOptions from model config | ||
| const userProviderOptions = | ||
| typeof options?.model === "object" | ||
| ? (options.model as Record<string, unknown>).providerOptions | ||
| : undefined; |
There was a problem hiding this comment.
The extraction of providerOptions lacks type safety. providerOptions is not defined in the official AgentModelConfig or ModelConfiguration types, so TypeScript won't catch if someone passes a non-object value (e.g., providerOptions: "invalid").
Consider adding validation or explicitly typing providerOptions in AgentModelConfig:
// In types/public/agent.ts
export type AgentModelConfig<TModelName extends string = string> = {
modelName: TModelName;
providerOptions?: Record<string, unknown>;
} & Record<string, unknown>;Then add runtime validation:
const userProviderOptions =
typeof options?.model === "object"
? (options.model as Record<string, unknown>).providerOptions
: undefined;
if (userProviderOptions !== undefined && typeof userProviderOptions !== "object") {
throw new StagehandInvalidArgumentError(
"providerOptions must be an object"
);
}Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/lib/v3/v3.ts
Line: 1611:1615
Comment:
The extraction of `providerOptions` lacks type safety. `providerOptions` is not defined in the official `AgentModelConfig` or `ModelConfiguration` types, so TypeScript won't catch if someone passes a non-object value (e.g., `providerOptions: "invalid"`).
Consider adding validation or explicitly typing `providerOptions` in `AgentModelConfig`:
```typescript
// In types/public/agent.ts
export type AgentModelConfig<TModelName extends string = string> = {
modelName: TModelName;
providerOptions?: Record<string, unknown>;
} & Record<string, unknown>;
```
Then add runtime validation:
```typescript
const userProviderOptions =
typeof options?.model === "object"
? (options.model as Record<string, unknown>).providerOptions
: undefined;
if (userProviderOptions !== undefined && typeof userProviderOptions !== "object") {
throw new StagehandInvalidArgumentError(
"providerOptions must be an object"
);
}
```
How can I resolve this? If you propose a fix, please make it concise.| private deepMerge( | ||
| target: Record<string, unknown>, | ||
| source: Record<string, unknown>, | ||
| ): Record<string, unknown> { | ||
| const result = { ...target }; | ||
| for (const key of Object.keys(source)) { | ||
| const sourceVal = source[key]; | ||
| const targetVal = target[key]; | ||
| if ( | ||
| sourceVal !== null && | ||
| typeof sourceVal === "object" && | ||
| !Array.isArray(sourceVal) && | ||
| targetVal !== null && | ||
| typeof targetVal === "object" && | ||
| !Array.isArray(targetVal) | ||
| ) { | ||
| result[key] = this.deepMerge( | ||
| targetVal as Record<string, unknown>, | ||
| sourceVal as Record<string, unknown>, | ||
| ); | ||
| } else { | ||
| result[key] = sourceVal; | ||
| } | ||
| } | ||
| return result; | ||
| } |
There was a problem hiding this comment.
The deepMerge function will copy undefined values from source to target, which could cause issues if the AI SDK doesn't handle undefined values in providerOptions properly.
When a user passes { google: { someOption: undefined } }, this will set result.google.someOption = undefined rather than omitting the key.
Consider filtering out undefined values:
private deepMerge(
target: Record<string, unknown>,
source: Record<string, unknown>,
): Record<string, unknown> {
const result = { ...target };
for (const key of Object.keys(source)) {
const sourceVal = source[key];
const targetVal = target[key];
// Skip undefined values
if (sourceVal === undefined) {
continue;
}
if (
sourceVal !== null &&
typeof sourceVal === "object" &&
!Array.isArray(sourceVal) &&
targetVal !== null &&
typeof targetVal === "object" &&
!Array.isArray(targetVal)
) {
result[key] = this.deepMerge(
targetVal as Record<string, unknown>,
sourceVal as Record<string, unknown>,
);
} else {
result[key] = sourceVal;
}
}
return result;
}Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/lib/v3/handlers/v3AgentHandler.ts
Line: 107:132
Comment:
The `deepMerge` function will copy `undefined` values from source to target, which could cause issues if the AI SDK doesn't handle undefined values in providerOptions properly.
When a user passes `{ google: { someOption: undefined } }`, this will set `result.google.someOption = undefined` rather than omitting the key.
Consider filtering out undefined values:
```typescript
private deepMerge(
target: Record<string, unknown>,
source: Record<string, unknown>,
): Record<string, unknown> {
const result = { ...target };
for (const key of Object.keys(source)) {
const sourceVal = source[key];
const targetVal = target[key];
// Skip undefined values
if (sourceVal === undefined) {
continue;
}
if (
sourceVal !== null &&
typeof sourceVal === "object" &&
!Array.isArray(sourceVal) &&
targetVal !== null &&
typeof targetVal === "object" &&
!Array.isArray(targetVal)
) {
result[key] = this.deepMerge(
targetVal as Record<string, unknown>,
sourceVal as Record<string, unknown>,
);
} else {
result[key] = sourceVal;
}
}
return result;
}
```
How can I resolve this? If you propose a fix, please make it concise.| private buildProviderOptions( | ||
| modelId: string, | ||
| ): SharedV2ProviderOptions | undefined { | ||
| const isGemini3 = modelId.includes("gemini-3"); |
There was a problem hiding this comment.
The Gemini 3 detection using modelId.includes("gemini-3") is simple but could potentially match unintended model IDs like "custom-gemini-3-wrapper" or "gemini-30".
While the current test coverage suggests this works for expected formats, consider using a more precise pattern match:
const isGemini3 = /gemini-3(?:-|$)/.test(modelId);This ensures "gemini-3" is followed by a hyphen or end of string, preventing false matches on "gemini-30" or other variants.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/lib/v3/handlers/v3AgentHandler.ts
Line: 84:84
Comment:
The Gemini 3 detection using `modelId.includes("gemini-3")` is simple but could potentially match unintended model IDs like "custom-gemini-3-wrapper" or "gemini-30".
While the current test coverage suggests this works for expected formats, consider using a more precise pattern match:
```typescript
const isGemini3 = /gemini-3(?:-|$)/.test(modelId);
```
This ensures "gemini-3" is followed by a hyphen or end of string, preventing false matches on "gemini-30" or other variants.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
Summary
Adds support for users to pass custom
providerOptionsto the agent viaAgentModelConfig. This enables features like Gemini's reasoning/thinking output (thinkingConfig) that were previously inaccessible due to hardcodedproviderOptions.V3AgentHandlerconstructor to acceptuserProviderOptionsbuildProviderOptions()method to merge user options with Gemini 3 defaultsdeepMerge()helper for nested object mergingproviderOptionsfrom model config inv3.tsMotivation
Resolves #1524
Stagehand hardcodes
providerOptionsfor Gemini 3 models, only settingmediaResolution: "MEDIA_RESOLUTION_HIGH". This prevents users from configuring provider-specific options likethinkingConfig: { includeThoughts: true, thinkingBudget: 8192 }which is required to access Gemini's reasoning/thinking output.Usage
Test plan
buildProviderOptionsanddeepMergefunctionalityvertex/gemini-2.5-flash+thinkingBudget: 8192- reasoning output works🤖 Generated with Claude Code
Summary by cubic
Adds support for user-provided providerOptions in AgentModelConfig, enabling provider-specific features like Gemini reasoning output. For Gemini 3 models, we still apply the default mediaResolution and merge user options on top.
Written for commit a1b4e74. Summary will update on new commits.