Skip to content

[Bug]: stagehand example's workersAIClient.ts fails typecheck against regenerated Workers AI types #227

Description

@garysassano

Version

packages/playwright-cloudflare/examples/stagehand @ 7c31261.

Steps to reproduce

The example typechecks in this repo only because it commits a worker-configuration.d.ts generated back in September:

// Runtime types generated with workerd@1.20250902.0 2025-09-01 enable_nodejs_fs_module,nodejs_compat

Regenerating the types breaks it:

git clone --depth 1 https://github.com/cloudflare/playwright
cd playwright/packages/playwright-cloudflare/examples/stagehand
npm install
npx tsc --noEmit        # clean, using the committed worker-configuration.d.ts
npx wrangler types      # regenerate against the installed wrangler
npx tsc --noEmit        # fails

This matters outside the repo because the Browser Run Stagehand docs tell users to "Copy workersAIClient.ts to your project", where they generate their own types instead of inheriting the committed one.

Expected behavior

The example typechecks against the Workers AI types that its own pinned wrangler generates:

Actual behavior

Two different failures depending on the wrangler version, both originating in the same call:

this.options?.logger?.({ category: "workersai", message: "thinking..." });
const { response } = await this.binding.run(this.modelName as keyof AiModels, {
messages: options.messages,
// @ts-ignore
tools: options.tools,
response_format: schema ? {
type: "json_schema",
json_schema: zodToJsonSchema(schema),
} : undefined,
temperature: 0,
}, this.options) as AiTextGenerationOutput;

wrangler generated runtime error
4.57.0 (resolved from the ^4.57.0 above) workerd@1.20260103.0 TS2322 at messages: Type 'ChatMessage[]' is not assignable to type '{ role: string; content: string; }[]'
4.125.0 (current) workerd@1.20260820.x TS2769 at the model argument: Argument of type 'keyof AiModels' is not assignable to parameter of type 'never'

The root cause is this.modelName as keyof AiModels on line 32. Widening the model id from a literal to the full union means no known-model overload can match, because inputs then has to satisfy every model's input type at once. Resolution falls through to the unknown-model overload, whose Model extends keyof AiModelList ? never : Model constraint is built to reject known keys, which is where the never comes from. That constraint is deliberate and documented in the current generated types:

// Unknown model (fallback).
//
// The `Exclude<..., keyof AiModelList>` constraint forces TypeScript to
// route any model name that is a literal key of `AiModelList` to one of
// the known-model overloads above ...
run<Model extends string>(model: Model extends keyof AiModelList ? never : Model, ...)

That overload does not exist in the committed September types, which is why the problem is invisible here.

The existing // @ts-ignore on line 34 does not help either: it applies to tools on line 35, while the overload-resolution error is reported on the model argument.

Additional context

This patch typechecks cleanly in all three combinations I tried: the committed September types, wrangler 4.57.0 regenerated types, and wrangler 4.125.0 regenerated types. Keeping the literal lets the known-model overload resolve, and one explicit cast covers the genuine mismatch (Stagehand's message and tool shapes are structurally looser than the generated input types) rather than a directive pointed at the wrong line.

-const modelId = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
+const modelId = "@cf/meta/llama-3.3-70b-instruct-fp8-fast" as const;
+
+type ModelInputs = AiModels[typeof modelId]["inputs"];
@@
-    const { response } = await this.binding.run(this.modelName as keyof AiModels, {
+    // Stagehand's message and tool shapes are structurally looser than the
+    // generated Workers AI input types, but the model accepts them at runtime.
+    const inputs = {
       messages: options.messages,
-      // @ts-ignore
       tools: options.tools,
       response_format: schema ? {
         type: "json_schema",
         json_schema: zodToJsonSchema(schema),
       } : undefined,
       temperature: 0,
-    }, this.options) as AiTextGenerationOutput;
+    } as unknown as ModelInputs;
+
+    const { response } = await this.binding.run(modelId, inputs, this.options) as AiTextGenerationOutput;

Happy to open a PR with this if it is useful.

Separately: regenerating worker-configuration.d.ts in CI, or typechecking the examples against current types, would catch this class of drift before it reaches anyone copying the file.

Environment

Node.js v24
npm, Linux x64 (WSL2)
wrangler 4.57.0 (from this example's lockfile) and 4.125.0
typescript 5.8.3 (from this example) and 7.0.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions