Skip to content

[fix]: support custom model base URL and legacy chatcompletions endpoints#1871

Open
shrey150 wants to merge 14 commits intomainfrom
pr1804-analysis
Open

[fix]: support custom model base URL and legacy chatcompletions endpoints#1871
shrey150 wants to merge 14 commits intomainfrom
pr1804-analysis

Conversation

@shrey150
Copy link
Contributor

@shrey150 shrey150 commented Mar 23, 2026

why

Generated Stagehand API / Stainless SDK users can already configure custom model settings locally, but custom OpenAI-compatible base URLs were not fully carried through the Stagehand API path. That makes it hard to use customer-provided endpoints, especially providers that only expose legacy /chat/completions.

This branch repackages Chris Read's model-base-url work on top of current main and includes the follow-up fixes I found while validating it.

what changed

  • add support for chatcompletions/<model> so OpenAI-compatible requests can explicitly use the Chat Completions path
  • thread model_base_url / x-model-base-url through the generated Stagehand API + SDK path so API/SDK users can target custom OpenAI-compatible endpoints
  • keep the no-schema fallback working for models that do not support structured outputs, with sanitized fallback errors
  • add targeted unit coverage for chatcompletions model construction with custom baseURL
  • add targeted v3 integration coverage for chatcompletions session start and x-model-base-url extract requests
  • rebase onto current main without restoring deleted old server-v4 /sessions/* runtime/test files; keep the still-relevant shared API/header/OpenAPI/Stainless propagation that survives on current main

test plan

  • pnpm --filter @browserbasehq/stagehand build:esm
  • pnpm --filter @browserbasehq/stagehand typecheck
  • pnpm --filter @browserbasehq/stagehand test:core -- packages/core/dist/esm/tests/unit/llm-provider.test.js
  • pnpm --filter @browserbasehq/stagehand-server-v3 build
  • pnpm --filter @browserbasehq/stagehand-server-v3 typecheck
  • pnpm --filter @browserbasehq/stagehand-server-v4 build
  • pnpm --filter @browserbasehq/stagehand-server-v4 typecheck
  • OPENAI_API_KEY=test-key CHROME_PATH='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' STAGEHAND_SERVER_TARGET=local STAGEHAND_BASE_URL='http://127.0.0.1:3157' pnpm --filter @browserbasehq/stagehand-server-v3 test:server -- packages/server-v3/dist/tests/integration/v3/start.test.js -- --test-name-pattern='accept chatcompletions-prefixed model names'
  • OPENAI_API_KEY=test-key CHROME_PATH='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' STAGEHAND_SERVER_TARGET=local STAGEHAND_BASE_URL='http://127.0.0.1:3158' pnpm --filter @browserbasehq/stagehand-server-v3 test:server -- packages/server-v3/dist/tests/integration/v3/extract.test.js -- --test-name-pattern='use x-model-base-url for chatcompletions extract requests'

@changeset-bot
Copy link

changeset-bot bot commented Mar 23, 2026

🦋 Changeset detected

Latest commit: 04dcf91

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

This PR includes changesets to release 5 packages
Name Type
@browserbasehq/stagehand Minor
@browserbasehq/stagehand-server-v3 Minor
@browserbasehq/stagehand-server-v4 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

@shrey150 shrey150 changed the title Follow up: cover chatcompletions support gaps Add custom model base URL and chatcompletions compatibility Mar 23, 2026
@shrey150 shrey150 changed the base branch from chris/model-base-url to main March 23, 2026 17:22
@shrey150 shrey150 marked this pull request as ready for review March 23, 2026 17:22
Thread modelBaseURL from x-model-base-url header through to V3 options,
enabling providers like ZhipuAI, Ollama, and other OpenAI-compatible
endpoints. Uses Chat Completions API (not Responses API) when a custom
baseURL is set, and adds robust response coercion for models without
native structured output support.
Adds "chatcompletions" as a generic provider that uses the Chat
Completions API (/chat/completions) instead of the Responses API,
for endpoints like ZhipuAI and Ollama. Also simplifies response
coercion for models without native structured output support.
Try structured output (schema:) first for all models. Only fall back
to no-schema + response coercion when the call fails and the model
matches a known fallback pattern. This avoids degrading DeepSeek/Kimi
which already work with schema:.
- Skip schema attempt for chatcompletions/ models (provider: openai.chat)
  since they can't do structured output — avoids a wasted LLM call per extract
- Unify .chat() handling in getAISDKLanguageModel so chatcompletions/ works
  regardless of whether clientOptions are provided
- Guard second schema.parse() with safeParse + descriptive error message
Copy link
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.

7 issues found across 26 files

Confidence score: 3/5

  • There is concrete regression risk in packages/core/lib/v3/llm/aisdk.ts: a raw upstream error is rethrown instead of a sanitized typed error, which can leak unsanitized messages and break expected error handling paths.
  • packages/core/lib/v3/llm/aisdk.ts also has a likely runtime-failure path (Object.entries on noSchemaResponse.object without guarding non-object output), plus broad string-to-JSON parsing that may corrupt valid string fields.
  • Test reliability concerns are present in packages/server-v3/test/integration/v3/extract.test.ts and packages/server-v4/test/integration/v4/extract.test.ts, where startup failures can bypass cleanup and leak servers/Chrome processes; this is risky but mostly scoped to test/integration behavior.
  • Pay close attention to packages/core/lib/v3/llm/aisdk.ts, packages/server-v3/test/integration/v3/extract.test.ts, packages/server-v4/test/integration/v4/extract.test.ts, packages/core/tests/unit/llm-provider.test.ts - error sanitization/runtime guards and cleanup/test coverage gaps are the main risk drivers.
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/core/lib/v3/llm/aisdk.ts">

<violation number="1" location="packages/core/lib/v3/llm/aisdk.ts:248">
P1: Custom agent: **Exception and error message sanitization**

Rule 1 violation: this rethrows a raw upstream error instead of throwing a sanitized typed error class. Wrap it in `CreateChatCompletionResponseError` (or another typed sanitized SDK error) before raising it.</violation>

<violation number="2" location="packages/core/lib/v3/llm/aisdk.ts:264">
P1: Guard `noSchemaResponse.object` before calling `Object.entries` to avoid runtime crashes on non-object model output.</violation>

<violation number="3" location="packages/core/lib/v3/llm/aisdk.ts:268">
P2: Only parse string fields that look like JSON containers; parsing all strings can corrupt valid string values.</violation>
</file>

<file name="packages/core/tests/unit/llm-provider.test.ts">

<violation number="1" location="packages/core/tests/unit/llm-provider.test.ts:68">
P2: The custom `baseURL` test does not validate `baseURL` behavior; it only repeats the same assertions as the no-options case, so regressions in forwarding `clientOptions.baseURL` would go undetected.

(Based on your team's feedback about adding focused unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/server-v3/test/integration/v3/extract.test.ts">

<violation number="1" location="packages/server-v3/test/integration/v3/extract.test.ts:500">
P2: Resource acquisition happens before the `try/finally`, so startup failures can bypass cleanup and leak test processes/servers.</violation>
</file>

<file name="packages/server-v4/test/integration/v4/extract.test.ts">

<violation number="1" location="packages/server-v4/test/integration/v4/extract.test.ts:142">
P2: Add failure-path cleanup in `startLocalChromeWithCdp`; rejected startup currently leaks spawned Chrome/temp-dir resources.</violation>

<violation number="2" location="packages/server-v4/test/integration/v4/extract.test.ts:500">
P2: Move resource startup into the guarded cleanup flow; a startup failure currently skips `finally` and leaks the fake chat server.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant SDK as Stagehand SDK
    participant API as API Client (V3)
    participant Srv as Stagehand Server (v3/v4)
    participant Core as LLMProvider (Core)
    participant AISDK as AI SDK Logic
    participant LLM as OpenAI-Compatible API

    Note over SDK,LLM: NEW: Request Flow with Custom Base URL and chatcompletions Provider

    SDK->>API: init(modelBaseURL, modelName: "chatcompletions/...")
    API->>Srv: POST /sessions/start
    Note right of API: NEW: Includes x-model-base-url header
    
    Srv->>Srv: CHANGED: Extract modelBaseURL from header/body
    Srv-->>API: 201 Created (Session ID)
    API-->>SDK: Session Initialized

    Note over SDK,LLM: Runtime Execution (e.g., extract())

    SDK->>Srv: POST /sessions/:id/extract
    Srv->>Core: getAISDKLanguageModel(provider, model, options)
    
    alt NEW: Provider is "chatcompletions"
        Core->>Core: Map to createOpenAI() with custom baseURL
        Core-->>Srv: Return OpenAI instance in .chat mode
    end

    Srv->>AISDK: generateObject(model, schema, messages)

    alt NEW: Model is "chatcompletions" or Structured Output Fails
        AISDK->>LLM: NEW: Fetch via /chat/completions (no-schema)
        LLM-->>AISDK: Raw JSON String
        
        AISDK->>AISDK: NEW: Fix stringified values (JSON.parse)
        AISDK->>AISDK: NEW: Coerce missing arrays to []
        
        alt Validation Success
            AISDK-->>Srv: Validated Object
        else Validation Failure
            AISDK-->>Srv: Throw CreateChatCompletionResponseError
        end
    else Standard Provider
        AISDK->>LLM: Standard Structured Output Call
        LLM-->>AISDK: JSON Object
        AISDK-->>Srv: Validated Object
    end

    Srv-->>SDK: SSE / JSON Response (Extracted Data)
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

expect((model as { provider?: string }).provider).toBe("openai.chat");
});

it("uses the OpenAI chat provider with custom baseURL", () => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 23, 2026

Choose a reason for hiding this comment

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

P2: The custom baseURL test does not validate baseURL behavior; it only repeats the same assertions as the no-options case, so regressions in forwarding clientOptions.baseURL would go undetected.

(Based on your team's feedback about adding focused unit tests for new behavior.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/tests/unit/llm-provider.test.ts, line 68:

<comment>The custom `baseURL` test does not validate `baseURL` behavior; it only repeats the same assertions as the no-options case, so regressions in forwarding `clientOptions.baseURL` would go undetected.

(Based on your team's feedback about adding focused unit tests for new behavior.) </comment>

<file context>
@@ -57,6 +57,24 @@ describe("getAISDKLanguageModel", () => {
+      expect((model as { provider?: string }).provider).toBe("openai.chat");
+    });
+
+    it("uses the OpenAI chat provider with custom baseURL", () => {
+      const model = getAISDKLanguageModel("chatcompletions", "glm-4-flash", {
+        baseURL: "https://open.bigmodel.cn/api/paas/v4",
</file context>
Fix with Cubic

@github-actions
Copy link
Contributor

github-actions bot commented Mar 23, 2026

✱ Stainless preview builds

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

feat: Add custom model base URL and chatcompletions compatibility

Edit this comment to update it. It will appear in the SDK's changelogs.

⚠️ stagehand-python studio · code · diff

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗ (prev: lint ✅) → test ❗ (prev: test ✅)

pip install https://pkg.stainless.com/s/stagehand-python/be9150892fea6b67280c4f77e5990f28af777007/stagehand-3.6.0-py3-none-any.whl
stagehand-typescript studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/stagehand-typescript/e73a03c4647004c52df9f0c6441d1e1094d714d6/dist.tar.gz
⚠️ stagehand-ruby studio · code · diff

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗ (prev: lint ✅) → test ❗ (prev: test ✅)

stagehand-go studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ✅test ✅

go get github.com/stainless-sdks/stagehand-go@9c65f6c40d1a624658673859c37b7e0f92188d2a
stagehand-openapi studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅

stagehand-php studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅lint ✅test ✅

stagehand-java studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ✅ (prev: lint ❗) → test ✅

Add the following URL as a Maven source: 'https://pkg.stainless.com/s/stagehand-java/9c44e0b1ee947ee38621b831387bfd8ad3fe0c3c/mvn'
stagehand-kotlin studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ✅lint ✅test ✅

stagehand-csharp studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️build ❗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-03-23 18:04:26 UTC

@shrey150 shrey150 changed the title Add custom model base URL and chatcompletions compatibility [fix]: support custom model base URL and legacy chatcompletions endpoints Mar 23, 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.

2 participants