Skip to content

Commit 8384272

Browse files
committed
fix: forward fetch and headers options to AI SDK providers
1 parent 8ff7d27 commit 8384272

File tree

3 files changed

+111
-2
lines changed

3 files changed

+111
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
fix: forward fetch and headers options to AI SDK providers to enable proxy authentication, request logging, and custom retry logic
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Stagehand } from "../lib/v3";
2+
3+
/**
4+
* Test script to verify custom fetch and headers are forwarded to AI SDK providers
5+
*
6+
* This demonstrates the fix for the bug where custom fetch functions and headers
7+
* were being silently ignored when using AI SDK providers (e.g., "openai/gpt-4o-mini").
8+
*
9+
* Expected behavior:
10+
* - Custom fetch function should be called for all LLM API requests
11+
* - Custom headers should be included in the requests
12+
* - This enables use cases like: proxy authentication, request logging, retry logic
13+
*/
14+
15+
async function main() {
16+
// Track if custom fetch was called
17+
let fetchCallCount = 0;
18+
const customHeaders: string[] = [];
19+
20+
// Create custom fetch function
21+
const customFetch: typeof fetch = async (url, options) => {
22+
fetchCallCount++;
23+
console.log(`✅ Custom fetch called (${fetchCallCount} times)`);
24+
console.log(` URL: ${url}`);
25+
26+
// Log custom headers if present
27+
if (options?.headers) {
28+
const headers = new Headers(options.headers);
29+
headers.forEach((value, key) => {
30+
if (key.toLowerCase().startsWith('x-custom')) {
31+
customHeaders.push(`${key}: ${value}`);
32+
console.log(` Custom header: ${key}: ${value}`);
33+
}
34+
});
35+
}
36+
37+
return fetch(url, options);
38+
};
39+
40+
// Initialize Stagehand with custom fetch and headers
41+
console.log("Initializing Stagehand with custom fetch and headers...\n");
42+
43+
const stagehand = new Stagehand({
44+
model: {
45+
modelName: "openai/gpt-4o-mini",
46+
apiKey: process.env.OPENAI_API_KEY,
47+
fetch: customFetch,
48+
headers: {
49+
"X-Custom-Header": "test-value",
50+
"X-Custom-Proxy-Auth": "proxy-token-123"
51+
}
52+
} as any,
53+
env: "LOCAL"
54+
});
55+
56+
await stagehand.init();
57+
58+
try {
59+
console.log("Making a simple LLM call via act()...\n");
60+
61+
// Navigate to a simple page
62+
await stagehand.context.pages()[0].goto("https://example.com");
63+
64+
// Make an act() call that will use the LLM
65+
await stagehand.act("find the heading on the page");
66+
67+
console.log("\n=== Test Results ===");
68+
if (fetchCallCount > 0) {
69+
console.log(`✅ SUCCESS: Custom fetch was called ${fetchCallCount} times`);
70+
console.log(`✅ Custom headers detected: ${customHeaders.length > 0 ? customHeaders.join(", ") : "None (may be overridden by SDK)"}`);
71+
} else {
72+
console.log("❌ FAILURE: Custom fetch was NOT called");
73+
console.log(" This indicates the bug still exists.");
74+
}
75+
} catch (error) {
76+
console.error("\n❌ Error during test:", error);
77+
} finally {
78+
await stagehand.close();
79+
}
80+
}
81+
82+
main().catch(console.error);

packages/core/lib/v3/llm/LLMProvider.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import { GoogleClient } from "./GoogleClient";
1616
import { GroqClient } from "./GroqClient";
1717
import { LLMClient } from "./LLMClient";
1818
import { OpenAIClient } from "./OpenAIClient";
19+
20+
interface ExtendedClientOptions {
21+
headers?: Record<string, string>;
22+
fetch?: typeof globalThis.fetch;
23+
}
1924
import { openai, createOpenAI } from "@ai-sdk/openai";
2025
import { anthropic, createAnthropic } from "@ai-sdk/anthropic";
2126
import { google, createGoogleGenerativeAI } from "@ai-sdk/google";
@@ -98,6 +103,8 @@ export function getAISDKLanguageModel(
98103
subModelName: string,
99104
apiKey?: string,
100105
baseURL?: string,
106+
headers?: Record<string, string>,
107+
fetch?: typeof globalThis.fetch,
101108
) {
102109
if (apiKey) {
103110
const creator = AISDKProvidersWithAPIKey[subProvider];
@@ -107,15 +114,28 @@ export function getAISDKLanguageModel(
107114
Object.keys(AISDKProvidersWithAPIKey),
108115
);
109116
}
110-
// Create the provider instance with the API key and baseURL if provided
111-
const providerConfig: { apiKey: string; baseURL?: string } = { apiKey };
117+
// Create the provider instance with the API key and custom options
118+
const providerConfig: {
119+
apiKey: string;
120+
baseURL?: string;
121+
headers?: Record<string, string>;
122+
fetch?: typeof globalThis.fetch;
123+
} = { apiKey };
112124
if (baseURL) {
113125
providerConfig.baseURL = baseURL;
114126
}
127+
if (headers) {
128+
providerConfig.headers = headers;
129+
}
130+
if (fetch) {
131+
providerConfig.fetch = fetch;
132+
}
115133
const provider = creator(providerConfig);
116134
// Get the specific model from the provider
117135
return provider(subModelName);
118136
} else {
137+
// When no apiKey is provided, use pre-configured provider (no custom options)
138+
// Note: headers and fetch options require explicit apiKey to be forwarded
119139
const provider = AISDKProviders[subProvider];
120140
if (!provider) {
121141
throw new UnsupportedAISDKModelProviderError(
@@ -148,6 +168,8 @@ export class LLMProvider {
148168
subModelName,
149169
clientOptions?.apiKey,
150170
clientOptions?.baseURL,
171+
(clientOptions as ExtendedClientOptions)?.headers,
172+
(clientOptions as ExtendedClientOptions)?.fetch,
151173
);
152174

153175
return new AISdkClient({

0 commit comments

Comments
 (0)