cloudflare / cloudflare/playwright

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

Open Beginner friendly
#227 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
121
Forks
27
Avg merge
2h 35m
Merged PRs (30d)
1

Description

### Version

`packages/playwright-cloudflare/examples/stagehand` @ [`7c31261`](https://github.com/cloudflare/playwright/commit/7c31261d6975b50772867026502aca60000f410e).

### Steps to reproduce

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

https://github.com/cloudflare/playwright/blob/7c31261d6975b50772867026502aca60000f410e/packages/playwright-cloudflare/examples/stagehand/worker-configuration.d.ts#L3

Regenerating the types breaks it:

```sh
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](https://developers.cloudflare.com/browser-run/stagehand/) 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:

https://github.com/cloudflare/playwright/blob/7c31261d6975b50772867026502aca60000f410e/packages/playwright-cloudflare/examples/stagehand/package.json#L22

### Actual behavior

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

https://github.com/cloudflare/playwright/blob/7c31261d6975b50772867026502aca60000f410e/packages/playwright-cloudflare/examples/stagehand/src/worker/workersAIClient.ts#L31-L41

| 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:

```ts
// 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: 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.

```diff
-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
```

Contributor guide

Open the contributing guide

Research direction

Start with packages/playwright-cloudflare/examples/stagehand/src/worker/workersAIClient.ts and the pinned Wrangler version in package.json. Run npx wrangler types followed by npx tsc --noEmit in the stagehand example, then verify the example typechecks with the regenerated worker-configuration.d.ts and the supported Wrangler versions described in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
ai, cloud, testing
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.