Skip to content

[STG-1756] forward Vertex model config#2160

Merged
monadoid merged 6 commits into
mainfrom
STG-1756-vertex-model-forwarding
May 27, 2026
Merged

[STG-1756] forward Vertex model config#2160
monadoid merged 6 commits into
mainfrom
STG-1756-vertex-model-forwarding

Conversation

@monadoid
Copy link
Copy Markdown
Contributor

@monadoid monadoid commented May 22, 2026

Why

Vertex model credentials need a typed public shape and must be forwarded from the TypeScript client to API-backed Stagehand requests without persisting secrets server-side.

What changed

  • Define a canonical Vertex model config shape using provider: "vertex", auth, and providerOptions.vertex, while preserving existing generic model string/object support.
  • Forward constructor model config through API-backed act/observe/extract/agentExecute requests, with per-call model config still taking precedence.
  • Keep navigate/goto from exposing a public per-call model option, while still allowing constructor config to bootstrap API-backed sessions internally.
  • Sync server-v3 request parsing with the hosted API approach: Zod-first safe parsing, separate request model config from Stagehand init model config, and clean SSE validation errors.
  • Regenerate server-v3 OpenAPI from the Stagehand Zod source so Stainless can pick up the canonical shape.

TODO before final hosted API merge: swap core to the Orca build produced from this Stagehand change.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

🦋 Changeset detected

Latest commit: e1515c5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@browserbasehq/stagehand Minor
@browserbasehq/stagehand-server-v3 Minor
@browserbasehq/browse-cli Patch
@browserbasehq/stagehand-evals Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

✱ Stainless preview builds for stagehand

This PR will update the stagehand SDKs with the following commit message.

feat: [STG-1756] forward Vertex model config
stagehand-openapi studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅

stagehand-go studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅build ✅lint ✅test ✅

go get github.com/stainless-sdks/stagehand-go@e707c1ca0b01fa7babe4ee081789766695e7d48e
stagehand-java studio · conflict

Your SDK build had at least one note diagnostic.

⚠️ stagehand-python studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ✅lint ✅test ✅

pip install https://pkg.stainless.com/s/stagehand-python/09bdd5484e09c3827bed41e8fca5dbefd6d9ada9/stagehand-3.20.0-py3-none-any.whl
⚠️ stagehand-typescript studio · conflict

Your SDK build had at least one warning diagnostic.

⚠️ stagehand-csharp studio · code

Your SDK build had a failure in the build CI job, which is a regression from the base state.
generate ⚠️build ❗lint ✅test ✅

stagehand-ruby studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅build ⏭️lint ✅test ✅

stagehand-kotlin studio · conflict

Your SDK build had at least one note diagnostic.

⚠️ stagehand-php studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️lint ✅test ✅


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-05-27 19:45:11 UTC

@monadoid monadoid marked this pull request as ready for review May 22, 2026 13:32
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.

2 issues found across 15 files

Confidence score: 3/5

  • There is a concrete regression risk in packages/core/lib/v3/api.ts: goto() does not route per-call model through prepareModelConfig(), so string models may behave differently than act/extract/observe and lead to incorrect model resolution at runtime.
  • packages/server-v3/src/lib/header.ts has a medium-severity precedence issue where body.modelName can leak into requests even when an object options.model/agentConfig.model is intended to be authoritative, which can cause unintended model selection.
  • Given the 6–7/10 severity and reasonably high confidence, this is a moderate user-impact risk rather than a merge-blocking failure; merge is possible, but a targeted fix would materially reduce regression chance.
  • Pay close attention to packages/core/lib/v3/api.ts and packages/server-v3/src/lib/header.ts - model normalization and fallback precedence can silently select the wrong model.
Architecture diagram
sequenceDiagram
    participant C as Client App
    participant V3 as V3 Core (v3.ts)
    participant APIClient as StagehandAPIClient (api.ts)
    participant Server as Server-v3 (Fastify)
    participant SessionStore as InMemorySessionStore
    participant Browser

    Note over C,Browser: Constructor Model Config Forwarding (Happy Path)

    C->>V3: new Stagehand({ modelName, modelClientOptions... })
    V3->>V3: store modelName + clientOptions
    C->>V3: stagehand.init()
    V3->>APIClient: init({ modelName, modelApiKey, defaultModelConfig })
    Note over APIClient: defaultModelConfig = merged modelName + clientOptions
    APIClient->>APIClient: store defaultModelConfig internally

    C->>V3: stagehand.navigate(url, options?)
    V3->>APIClient: goto(url, options)
    alt options.model provided per-call
        APIClient->>APIClient: use options.model (prepared)
    else no per-call model AND defaultModelConfig exists
        APIClient->>APIClient: use getDefaultModelConfig()
    end
    APIClient->>Server: POST /navigate { url, options.model }

    Server->>Server: getRequestModelConfig(body)
    Note over Server: parse body.options.model or body.agentConfig.model
    Server->>Server: delete options.model before page.goto

    alt session not yet started
        Server->>SessionStore: getOrCreateStagehand(sessionId, requestModelConfig)
        SessionStore->>SessionStore: create V3Options from requestModelConfig + modelApiKey fallback
        SessionStore-->>Server: stagehand instance
    end
    Server->>Browser: page.goto(url, strippedOptions)
    Browser-->>Server: navigation result
    Server-->>APIClient: response
    APIClient-->>V3: result
    V3-->>C: result

    Note over C,Browser: Act/Extract/Observe (same pattern)

    C->>V3: stagehand.act({ input, options? })
    V3->>APIClient: act({ input, options })
    alt options.model provided per-call
        APIClient->>APIClient: use options.model (prepared)
    else no per-call model AND defaultModelConfig exists
        APIClient->>APIClient: use getDefaultModelConfig()
    end
    APIClient->>Server: POST /act { options.model }
    Server->>Server: getRequestModelConfig(body) — extracts model config
    Server-->>APIClient: response
    APIClient-->>V3: result
    V3-->>C: result

    Note over C,Browser: Agent Execute (same pattern)

    C->>V3: stagehand.agentExecute(agentConfig, input)
    V3->>APIClient: agentExecute(agentConfig, input)
    alt agentConfig.model provided
        APIClient->>APIClient: prepareModelConfig(agentConfig.model)
    else no model AND defaultModelConfig exists
        APIClient->>APIClient: use getDefaultModelConfig()
    end
    APIClient->>Server: POST /agentExecute { agentConfig.model }
    Server->>Server: getRequestModelConfig(body) — extracts from agentConfig.model
    Server-->>APIClient: response
    APIClient-->>V3: result
    V3-->>C: result

    Note over C,Browser: Session Start Flow (Local Server)

    C->>Server: POST /sessions/start { modelName }
    Server->>Server: getRequestModelConfig(body)
    alt body has options.model or agentConfig.model
        Server->>Server: extract model config (including Vertex auth fields)
    else modelName only
        Server->>Server: use modelName + x-model-api-key header fallback
    end
    Server->>SessionStore: createSession({ modelName, requestModelConfig })
    SessionStore->>SessionStore: set ctx.requestModelConfig
    SessionStore->>SessionStore: create V3Options from ctx.requestModelConfig + modelApiKey
    Server-->>C: session created
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/core/lib/v3/api.ts
Comment thread packages/server-v3/src/lib/header.ts Outdated
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 12 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant ClientApp as Client Application
    participant V3 as V3 Instance
    participant APIClient as StagehandAPIClient
    participant Fetch as HTTP Client
    participant Server as Server-v3
    participant SessionStore as InMemorySessionStore
    participant LLM as LLMProvider
    participant Browser as Browser

    Note over ClientApp,Browser: Constructor Model Config Forwarding Flow

    ClientApp->>V3: new V3({ modelName: "vertex/...", modelClientOptions: { project, location, googleAuthOptions } })

    ClientApp->>V3: init()
    V3->>V3: getApiDefaultModelConfig()
    V3->>APIClient: init({ modelName, modelApiKey, defaultModelConfig: { modelName, project, location, googleAuthOptions } })
    APIClient->>APIClient: store defaultModelConfig

    alt navigate(url) called
        APIClient->>APIClient: strip model from publicOptions
        APIClient->>APIClient: getDefaultModelConfig() → Vertex config
        APIClient->>Fetch: POST /navigate { url, options: { model: Vertex config } }
        Fetch->>Server: Forward request
        Server->>Server: getRequestModelConfig() → parse body.options.model
        alt has model config
            Server->>SessionStore: bootstrap session with { modelName, project, location, googleAuthOptions }
            SessionStore->>LLM: getClient(Vertex model)
            SessionStore->>Browser: page.goto(url) – model stripped
        else fallback to header
            Server->>SessionStore: bootstrap with { modelName, apiKey: x-model-api-key }
        end
    end

    alt act/observe/extract() called
        alt per-call model provided
            APIClient->>APIClient: prepareModelConfig(per-call model)
            APIClient->>Fetch: POST /{act|observe|extract} { options: { model: per-call } }
        else no per-call model
            APIClient->>APIClient: getDefaultModelConfig() → use constructor config
            APIClient->>Fetch: POST /{act|observe|extract} { options: { model: constructor Vertex config } }
        end
        Fetch->>Server: Forward with model config
        Server->>SessionStore: Use request model config for LLM calls
    end

    alt agentExecute() called
        alt agentConfig.model provided
            APIClient->>APIClient: prepareModelConfig(agentConfig.model)
        else no agentConfig.model
            APIClient->>APIClient: getDefaultModelConfig() → use constructor config
        end
        APIClient->>Fetch: POST /agentExecute { agentConfig: { model: resolved config } }
        Fetch->>Server: Forward
    end

    Note over LLM: No longer throws ExperimentalNotConfiguredError for Vertex<br>Vertex models work without experimental mode

    alt constructor provides only modelApiKey (no modelClientOptions)
        V3->>V3: getApiDefaultModelConfig() → undefined
        APIClient->>APIClient: defaultModelConfig is undefined
        APIClient->>Fetch: No model in body, only x-model-api-key header
        Server->>SessionStore: bootstrap with { modelName, apiKey: x-model-api-key }
    end
Loading

Re-trigger cubic

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.

2 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/server-v3/src/routes/v1/sessions/start.ts">

<violation number="1" location="packages/server-v3/src/routes/v1/sessions/start.ts:218">
P1: Session leak: if model config validation fails here, the session created by `startSession()` above is never cleaned up. Add `await sessionStore.endSession(session.sessionId)` before returning the 400, similar to the error handling in the `catch` block below.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/server-v3/src/routes/v1/sessions/start.ts Outdated
Comment thread packages/core/lib/v3/types/public/api.ts Outdated
@monadoid monadoid merged commit 49575d6 into main May 27, 2026
219 of 220 checks passed
@github-actions github-actions Bot mentioned this pull request May 27, 2026
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.

3 participants