Skip to content

feat: Allow user-provided providerOptions in agent config#1525

Open
hoverlover wants to merge 1 commit intobrowserbase:mainfrom
hoverlover:feature/allow-user-provider-options
Open

feat: Allow user-provided providerOptions in agent config#1525
hoverlover wants to merge 1 commit intobrowserbase:mainfrom
hoverlover:feature/allow-user-provider-options

Conversation

@hoverlover
Copy link
Copy Markdown

@hoverlover hoverlover commented Jan 11, 2026

Summary

Adds support for users to pass custom providerOptions to the agent via AgentModelConfig. This enables features like Gemini's reasoning/thinking output (thinkingConfig) that were previously inaccessible due to hardcoded providerOptions.

  • Update V3AgentHandler constructor to accept userProviderOptions
  • Add buildProviderOptions() method to merge user options with Gemini 3 defaults
  • Add deepMerge() helper for nested object merging
  • Extract and pass providerOptions from model config in v3.ts
  • Add comprehensive test suite (14 tests) for the new functionality

Motivation

Resolves #1524

Stagehand hardcodes providerOptions for Gemini 3 models, only setting mediaResolution: "MEDIA_RESOLUTION_HIGH". This prevents users from configuring provider-specific options like thinkingConfig: { includeThoughts: true, thinkingBudget: 8192 } which is required to access Gemini's reasoning/thinking output.

Usage

const agent = stagehand.agent({
  model: {
    modelName: "vertex/gemini-2.5-flash",
    providerOptions: {
      google: {
        thinkingConfig: {
          includeThoughts: true,
          thinkingBudget: 8192,
        },
      },
    },
  },
  stream: true,
});

const result = await agent.execute({
  instruction: "Navigate to...",
  callbacks: {
    onStepFinish: ({ reasoning }) => {
      console.log("Reasoning:", reasoning);  // Now populated!
    }
  }
});

Test plan

  • All 322 existing vitest tests pass
  • Added 14 new tests for buildProviderOptions and deepMerge functionality
  • Manually tested with vertex/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.

  • New Features
    • Accept providerOptions in V3AgentHandler and pass them from v3.ts.
    • Merge user options with Gemini 3 defaults via buildProviderOptions and deepMerge; user values override defaults.
    • Apply providerOptions in both execute and stream paths, with tests covering merge behavior.

Written for commit a1b4e74. Summary will update on new commits.

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>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 11, 2026

⚠️ No Changeset found

Latest commit: a1b4e74

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 3 files

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jan 11, 2026

Greptile Overview

Greptile Summary

This PR successfully adds support for user-provided providerOptions in agent configuration, enabling access to provider-specific features like Gemini's reasoning output that were previously inaccessible due to hardcoded options.

Key Changes:

  • Adds userProviderOptions parameter to V3AgentHandler constructor
  • Implements buildProviderOptions() to intelligently merge user options with Gemini 3 defaults (mediaResolution: "MEDIA_RESOLUTION_HIGH")
  • Adds deepMerge() helper for nested object merging where user values take precedence
  • Extracts and passes providerOptions from model config in v3.ts
  • Consolidates duplicated providerOptions logic from both execute() and stream() methods
  • Includes 14 comprehensive tests covering merge behavior, Gemini 3 detection, and edge cases

Architecture:
The implementation correctly preserves backward compatibility by maintaining Gemini 3's default mediaResolution setting while allowing users to override or extend with additional options. The refactoring consolidates previously duplicated inline logic into a reusable method, improving maintainability.

Concerns:

  1. Type Safety: providerOptions is not officially defined in AgentModelConfig type definition, relying on Record<string, unknown> casting without runtime validation
  2. Undefined Handling: The deepMerge function copies undefined values from source to target, which could cause issues with the AI SDK if it doesn't expect undefined in providerOptions
  3. Pattern Matching: Gemini 3 detection uses simple string inclusion which could match unintended patterns like "gemini-30"

Testing:
Test coverage is solid for the core functionality but could be enhanced with validation tests for invalid input types and integration tests showing end-to-end usage.

Confidence Score: 4/5

  • This PR is safe to merge with minor refinements recommended for production robustness
  • The implementation is functionally sound with good test coverage (14 tests, all existing 322 tests pass). The core logic correctly merges user-provided options with defaults, and the refactoring consolidates duplicated code effectively. However, three style-level concerns prevent a perfect score: (1) lack of type safety for providerOptions extraction could allow runtime errors with invalid inputs, (2) deepMerge doesn't filter undefined values which may cause issues downstream, and (3) Gemini 3 detection pattern could match unintended model IDs. These are not blocking issues but should be addressed for production hardening.
  • packages/core/lib/v3/v3.ts requires validation for providerOptions type safety; packages/core/lib/v3/handlers/v3AgentHandler.ts should filter undefined values in deepMerge

Important Files Changed

File Analysis

Filename Score Overview
packages/core/lib/v3/handlers/v3AgentHandler.ts 4/5 Adds buildProviderOptions() and deepMerge() methods to merge user-provided providerOptions with Gemini 3 defaults. Good refactoring that consolidates duplicated logic. Minor concerns: deepMerge doesn't filter undefined values, type casting could be stricter.
packages/core/lib/v3/v3.ts 3/5 Extracts providerOptions from model config and passes to V3AgentHandler. Type safety concern: providerOptions is not officially typed in AgentModelConfig, relies on Record<string, unknown> casting without validation.
packages/core/tests/v3-agent-handler-provider-options.test.ts 4/5 Comprehensive test coverage (14 tests) for buildProviderOptions and deepMerge. Tests Gemini 3 detection, merging behavior, null handling, and non-mutation. Missing: undefined value handling, non-object providerOptions validation, integration tests.

Sequence Diagram

sequenceDiagram
    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
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +1611 to +1615
// Extract user-provided providerOptions from model config
const userProviderOptions =
typeof options?.model === "object"
? (options.model as Record<string, unknown>).providerOptions
: undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +107 to 132
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Expose Gemini 3 thinking/reasoning via includeThoughts in providerOptions

1 participant