Skip to content

Commit 6e684de

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

File tree

3 files changed

+108
-1
lines changed

3 files changed

+108
-1
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+
},
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: 21 additions & 1 deletion
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];
@@ -108,10 +115,21 @@ export function getAISDKLanguageModel(
108115
);
109116
}
110117
// Create the provider instance with the API key and baseURL if provided
111-
const providerConfig: { apiKey: string; baseURL?: string } = { apiKey };
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);
@@ -148,6 +166,8 @@ export class LLMProvider {
148166
subModelName,
149167
clientOptions?.apiKey,
150168
clientOptions?.baseURL,
169+
(clientOptions as ExtendedClientOptions)?.headers,
170+
(clientOptions as ExtendedClientOptions)?.fetch,
151171
);
152172

153173
return new AISdkClient({

0 commit comments

Comments
 (0)