Skip to content

Commit fea1700

Browse files
authored
Fix parametrized 'model' api key auto-loading (#1436)
# why Auto-loading different provider api keys isn't working on parametrized act/extract/observe `model` string definitions # what changed - Added prepareModelConfig helper method that ensures model configurations include the appropriate API key - When a model string or object is passed without an API key, the method: - Uses the default modelApiKey if the provider matches the one from init - Auto-loads the API key from environment variables (via `loadApiKeyFromEnv`) if the provider differs - Applied the helper to `act`, `extract`, and `observe` methods # test plan <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes API key auto-loading for parametrized model inputs in act, extract, and observe. Ensures the correct provider key is attached automatically, falling back to env vars when switching providers. - **Bug Fixes** - Added prepareModelConfig to attach apiKey to model strings/objects. - Detects provider from modelName and uses loadApiKeyFromEnv when it differs from the initialized provider; otherwise uses the default modelApiKey. - Applied to act, extract, and observe option handling. <sup>Written for commit 28d764b. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
1 parent b48c9c6 commit fea1700

File tree

2 files changed

+68
-0
lines changed

2 files changed

+68
-0
lines changed

.changeset/floppy-carpets-wish.md

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 auto-load key for act/extract/observe parametrized models on api

packages/core/lib/v3/api.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
import type { SerializableResponse } from "./types/private";
3131
import { toJsonSchema } from "./zodCompat";
3232
import type { StagehandZodSchema } from "./zodCompat";
33+
import { loadApiKeyFromEnv } from "../utils";
34+
import type { ModelConfiguration } from "./types/public/model";
3335

3436
/**
3537
* API response structure for replay metrics endpoint
@@ -58,6 +60,7 @@ export class StagehandAPIClient {
5860
private projectId: string;
5961
private sessionId?: string;
6062
private modelApiKey: string;
63+
private modelProvider?: string;
6164
private logger: (message: LogLine) => void;
6265
private fetchWithCookies;
6366

@@ -83,6 +86,10 @@ export class StagehandAPIClient {
8386
throw new StagehandAPIError("modelApiKey is required");
8487
}
8588
this.modelApiKey = modelApiKey;
89+
// Extract provider from modelName (e.g., "openai/gpt-4o" -> "openai")
90+
this.modelProvider = modelName?.includes("/")
91+
? modelName.split("/")[0]
92+
: undefined;
8693

8794
const region = browserbaseSessionCreateParams?.region;
8895
if (region && region !== "us-west-2") {
@@ -147,6 +154,9 @@ export class StagehandAPIClient {
147154
// eslint-disable-next-line @typescript-eslint/no-unused-vars
148155
const { page: _, ...restOptions } = options;
149156
if (Object.keys(restOptions).length > 0) {
157+
if (restOptions.model) {
158+
restOptions.model = this.prepareModelConfig(restOptions.model);
159+
}
150160
args.options = restOptions;
151161
}
152162
}
@@ -175,6 +185,9 @@ export class StagehandAPIClient {
175185
// eslint-disable-next-line @typescript-eslint/no-unused-vars
176186
const { page: _, ...restOptions } = options;
177187
if (Object.keys(restOptions).length > 0) {
188+
if (restOptions.model) {
189+
restOptions.model = this.prepareModelConfig(restOptions.model);
190+
}
178191
args.options = restOptions;
179192
}
180193
}
@@ -199,6 +212,9 @@ export class StagehandAPIClient {
199212
// eslint-disable-next-line @typescript-eslint/no-unused-vars
200213
const { page: _, ...restOptions } = options;
201214
if (Object.keys(restOptions).length > 0) {
215+
if (restOptions.model) {
216+
restOptions.model = this.prepareModelConfig(restOptions.model);
217+
}
202218
args.options = restOptions;
203219
}
204220
}
@@ -365,6 +381,53 @@ export class StagehandAPIClient {
365381
return metrics;
366382
}
367383

384+
/**
385+
* Prepares a model configuration for the API payload by ensuring
386+
* the apiKey is included. If the model is passed as a string,
387+
* it converts it to an object with modelName and apiKey.
388+
* The apiKey is loaded from environment variables only if the provider
389+
* differs from the one used during init.
390+
*/
391+
private prepareModelConfig(
392+
model: ModelConfiguration,
393+
): { modelName: string; apiKey: string } & Record<string, unknown> {
394+
if (typeof model === "string") {
395+
// Extract provider from model string (e.g., "openai/gpt-4o" -> "openai")
396+
const provider = model.includes("/") ? model.split("/")[0] : undefined;
397+
// Only load from env if provider differs from the original
398+
const apiKey =
399+
provider && provider !== this.modelProvider
400+
? (loadApiKeyFromEnv(provider, this.logger) ?? this.modelApiKey)
401+
: this.modelApiKey;
402+
return {
403+
modelName: model,
404+
apiKey,
405+
};
406+
}
407+
408+
// Model is an object - ensure apiKey is present
409+
if (!model.apiKey) {
410+
const provider = model.modelName?.includes("/")
411+
? model.modelName.split("/")[0]
412+
: undefined;
413+
// Only load from env if provider differs from the original
414+
const apiKey =
415+
provider && provider !== this.modelProvider
416+
? (loadApiKeyFromEnv(provider, this.logger) ?? this.modelApiKey)
417+
: this.modelApiKey;
418+
return {
419+
...model,
420+
apiKey,
421+
};
422+
}
423+
424+
// Model object already has apiKey
425+
return model as { modelName: string; apiKey: string } & Record<
426+
string,
427+
unknown
428+
>;
429+
}
430+
368431
private async execute<T>({
369432
method,
370433
args,

0 commit comments

Comments
 (0)