cloudflare / cloudflare/playwright
[Bug]: stagehand example's workersAIClient.ts fails typecheck against regenerated Workers AI types
- 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
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