-
Notifications
You must be signed in to change notification settings - Fork 778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix workers AI usage with @cloudflare/vite-plugin #8016
base: main
Are you sure you want to change the base?
Changes from all commits
0a5f772
aad1812
fad1123
b4d16b5
4da0265
f950bae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { expect, test } from "vitest"; | ||
import { getTextResponse } from "../../__test-utils__"; | ||
|
||
// todo(justinvdm, 2025-02-04): Since this example uses Workers AI, there is CLI interactivity required to choose and authenticate for the account | ||
// that should be used when talking to the Workers AI API, making it difficult to run this test automatically. Once the plugin accepts an account id | ||
// this test might be able to be unskipped. | ||
test.skip("basic hello-world functionality", async () => { | ||
expect(JSON.parse(await getTextResponse())).toContain("PONG"); | ||
}); | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "@playground/ai", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"build": "vite build --app", | ||
"check:types": "tsc --build", | ||
"dev": "vite dev", | ||
"preview": "vite preview" | ||
}, | ||
"devDependencies": { | ||
"@cloudflare/vite-plugin": "workspace:*", | ||
"@cloudflare/workers-tsconfig": "workspace:*", | ||
"@cloudflare/workers-types": "^4.20250129.0", | ||
"typescript": "catalog:default", | ||
"vite": "catalog:vite-plugin", | ||
"wrangler": "catalog:vite-plugin" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export interface Env { | ||
AI: Ai; | ||
} | ||
|
||
export default { | ||
async fetch(_request: Request, env: Env): Promise<Response> { | ||
const response = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", { | ||
prompt: "When I say PING, you say PONG. PING", | ||
}); | ||
|
||
return new Response( | ||
JSON.stringify((response as { response: string }).response.toUpperCase()) | ||
); | ||
}, | ||
} satisfies ExportedHandler<Env>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"files": [], | ||
"references": [ | ||
{ "path": "./tsconfig.node.json" }, | ||
{ "path": "./tsconfig.worker.json" } | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": ["@cloudflare/workers-tsconfig/base.json"], | ||
"include": ["vite.config.ts", "__tests__"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": ["@cloudflare/workers-tsconfig/worker.json"], | ||
"include": ["src"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"$schema": "http://turbo.build/schema.json", | ||
"extends": ["//"], | ||
"tasks": { | ||
"build": { | ||
"outputs": ["dist/**"] | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { cloudflare } from "@cloudflare/vite-plugin"; | ||
import { defineConfig } from "vite"; | ||
|
||
export default defineConfig({ | ||
plugins: [cloudflare({ persistState: false })], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
name = "worker" | ||
main = "./src/index.ts" | ||
compatibility_date = "2024-12-30" | ||
|
||
[ai] | ||
binding = "AI" |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -276,7 +276,7 @@ export function getDevMiniflareOptions( | |||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||
]; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const userWorkers = | ||||||||||||||||||||||||||||||||||||||||||
const workersFromConfig = | ||||||||||||||||||||||||||||||||||||||||||
resolvedPluginConfig.type === "workers" | ||||||||||||||||||||||||||||||||||||||||||
? Object.entries(resolvedPluginConfig.workers).map( | ||||||||||||||||||||||||||||||||||||||||||
([environmentName, workerConfig]) => { | ||||||||||||||||||||||||||||||||||||||||||
|
@@ -288,69 +288,99 @@ export function getDevMiniflareOptions( | |||||||||||||||||||||||||||||||||||||||||
resolvedPluginConfig.cloudflareEnv | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const { externalWorkers } = miniflareWorkerOptions; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const { ratelimits, ...workerOptions } = | ||||||||||||||||||||||||||||||||||||||||||
miniflareWorkerOptions.workerOptions; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||||||||||
...workerOptions, | ||||||||||||||||||||||||||||||||||||||||||
// We have to add the name again because `unstable_getMiniflareWorkerOptions` sets it to `undefined` | ||||||||||||||||||||||||||||||||||||||||||
name: workerConfig.name, | ||||||||||||||||||||||||||||||||||||||||||
modulesRoot: miniflareModulesRoot, | ||||||||||||||||||||||||||||||||||||||||||
unsafeEvalBinding: "__VITE_UNSAFE_EVAL__", | ||||||||||||||||||||||||||||||||||||||||||
bindings: { | ||||||||||||||||||||||||||||||||||||||||||
...workerOptions.bindings, | ||||||||||||||||||||||||||||||||||||||||||
__VITE_ROOT__: resolvedViteConfig.root, | ||||||||||||||||||||||||||||||||||||||||||
__VITE_ENTRY_PATH__: workerConfig.main, | ||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||
serviceBindings: { | ||||||||||||||||||||||||||||||||||||||||||
...workerOptions.serviceBindings, | ||||||||||||||||||||||||||||||||||||||||||
...(environmentName === | ||||||||||||||||||||||||||||||||||||||||||
resolvedPluginConfig.entryWorkerEnvironmentName && | ||||||||||||||||||||||||||||||||||||||||||
workerConfig.assets?.binding | ||||||||||||||||||||||||||||||||||||||||||
? { | ||||||||||||||||||||||||||||||||||||||||||
[workerConfig.assets.binding]: ASSET_WORKER_NAME, | ||||||||||||||||||||||||||||||||||||||||||
externalWorkers, | ||||||||||||||||||||||||||||||||||||||||||
worker: { | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The diff here is misleading - this was the only change for this part of code |
||||||||||||||||||||||||||||||||||||||||||
...workerOptions, | ||||||||||||||||||||||||||||||||||||||||||
// We have to add the name again because `unstable_getMiniflareWorkerOptions` sets it to `undefined` | ||||||||||||||||||||||||||||||||||||||||||
name: workerConfig.name, | ||||||||||||||||||||||||||||||||||||||||||
modulesRoot: miniflareModulesRoot, | ||||||||||||||||||||||||||||||||||||||||||
unsafeEvalBinding: "__VITE_UNSAFE_EVAL__", | ||||||||||||||||||||||||||||||||||||||||||
bindings: { | ||||||||||||||||||||||||||||||||||||||||||
...workerOptions.bindings, | ||||||||||||||||||||||||||||||||||||||||||
__VITE_ROOT__: resolvedViteConfig.root, | ||||||||||||||||||||||||||||||||||||||||||
__VITE_ENTRY_PATH__: workerConfig.main, | ||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||
serviceBindings: { | ||||||||||||||||||||||||||||||||||||||||||
...workerOptions.serviceBindings, | ||||||||||||||||||||||||||||||||||||||||||
...(environmentName === | ||||||||||||||||||||||||||||||||||||||||||
resolvedPluginConfig.entryWorkerEnvironmentName && | ||||||||||||||||||||||||||||||||||||||||||
workerConfig.assets?.binding | ||||||||||||||||||||||||||||||||||||||||||
? { | ||||||||||||||||||||||||||||||||||||||||||
[workerConfig.assets.binding]: ASSET_WORKER_NAME, | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
: {}), | ||||||||||||||||||||||||||||||||||||||||||
__VITE_INVOKE_MODULE__: async (request) => { | ||||||||||||||||||||||||||||||||||||||||||
const payload = | ||||||||||||||||||||||||||||||||||||||||||
(await request.json()) as vite.CustomPayload; | ||||||||||||||||||||||||||||||||||||||||||
const invokePayloadData = payload.data as { | ||||||||||||||||||||||||||||||||||||||||||
id: string; | ||||||||||||||||||||||||||||||||||||||||||
name: string; | ||||||||||||||||||||||||||||||||||||||||||
data: [string, string, FetchFunctionOptions]; | ||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
assert( | ||||||||||||||||||||||||||||||||||||||||||
invokePayloadData.name === "fetchModule", | ||||||||||||||||||||||||||||||||||||||||||
`Invalid invoke event: ${invokePayloadData.name}` | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const [moduleId] = invokePayloadData.data; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
// For some reason we need this here for cloudflare built-ins (e.g. `cloudflare:workers`) but not for node built-ins (e.g. `node:path`) | ||||||||||||||||||||||||||||||||||||||||||
// See https://github.com/flarelabs-net/vite-plugin-cloudflare/issues/46 | ||||||||||||||||||||||||||||||||||||||||||
if (moduleId.startsWith("cloudflare:")) { | ||||||||||||||||||||||||||||||||||||||||||
const result = { | ||||||||||||||||||||||||||||||||||||||||||
externalize: moduleId, | ||||||||||||||||||||||||||||||||||||||||||
type: "builtin", | ||||||||||||||||||||||||||||||||||||||||||
} satisfies vite.FetchResult; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return new MiniflareResponse(JSON.stringify({ result })); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
: {}), | ||||||||||||||||||||||||||||||||||||||||||
__VITE_INVOKE_MODULE__: async (request) => { | ||||||||||||||||||||||||||||||||||||||||||
const payload = (await request.json()) as vite.CustomPayload; | ||||||||||||||||||||||||||||||||||||||||||
const invokePayloadData = payload.data as { | ||||||||||||||||||||||||||||||||||||||||||
id: string; | ||||||||||||||||||||||||||||||||||||||||||
name: string; | ||||||||||||||||||||||||||||||||||||||||||
data: [string, string, FetchFunctionOptions]; | ||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
assert( | ||||||||||||||||||||||||||||||||||||||||||
invokePayloadData.name === "fetchModule", | ||||||||||||||||||||||||||||||||||||||||||
`Invalid invoke event: ${invokePayloadData.name}` | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const [moduleId] = invokePayloadData.data; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
// For some reason we need this here for cloudflare built-ins (e.g. `cloudflare:workers`) but not for node built-ins (e.g. `node:path`) | ||||||||||||||||||||||||||||||||||||||||||
// See https://github.com/flarelabs-net/vite-plugin-cloudflare/issues/46 | ||||||||||||||||||||||||||||||||||||||||||
if (moduleId.startsWith("cloudflare:")) { | ||||||||||||||||||||||||||||||||||||||||||
const result = { | ||||||||||||||||||||||||||||||||||||||||||
externalize: moduleId, | ||||||||||||||||||||||||||||||||||||||||||
type: "builtin", | ||||||||||||||||||||||||||||||||||||||||||
} satisfies vite.FetchResult; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return new MiniflareResponse(JSON.stringify({ result })); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const devEnvironment = viteDevServer.environments[ | ||||||||||||||||||||||||||||||||||||||||||
environmentName | ||||||||||||||||||||||||||||||||||||||||||
] as CloudflareDevEnvironment; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const result = await devEnvironment.hot.handleInvoke(payload); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return new MiniflareResponse(JSON.stringify(result)); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const devEnvironment = viteDevServer.environments[ | ||||||||||||||||||||||||||||||||||||||||||
environmentName | ||||||||||||||||||||||||||||||||||||||||||
] as CloudflareDevEnvironment; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const result = | ||||||||||||||||||||||||||||||||||||||||||
await devEnvironment.hot.handleInvoke(payload); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return new MiniflareResponse(JSON.stringify(result)); | ||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||
} satisfies Partial<WorkerOptions>; | ||||||||||||||||||||||||||||||||||||||||||
} satisfies Partial<WorkerOptions>, | ||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||
: []; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const userWorkers = workersFromConfig.map((worker) => worker.worker); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const resolvedExternalWorkersMap = new Map( | ||||||||||||||||||||||||||||||||||||||||||
workersFromConfig | ||||||||||||||||||||||||||||||||||||||||||
.flatMap((worker) => worker.externalWorkers) | ||||||||||||||||||||||||||||||||||||||||||
.map((worker) => [worker.name, worker]) | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const externalWorkersMap = new Map<string, WorkerOptions>(); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
for (const worker of userWorkers) { | ||||||||||||||||||||||||||||||||||||||||||
for (const binding of Object.values(worker.wrappedBindings ?? {})) { | ||||||||||||||||||||||||||||||||||||||||||
const scriptName = | ||||||||||||||||||||||||||||||||||||||||||
typeof binding === "string" ? binding : binding.scriptName; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const externalWorker = resolvedExternalWorkersMap.get(scriptName); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
if (externalWorker) { | ||||||||||||||||||||||||||||||||||||||||||
externalWorkersMap.set(scriptName, externalWorker); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this actually necessary? In Wrangler's I am pretty sure that Miniflare only adds external workers that are relevant to the bindings in the config. So if there is no AI binding there would be no equivalent external worker. See workers-sdk/packages/wrangler/src/dev/miniflare.ts Lines 589 to 608 in 444a630
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, while I think the filtering is not needed, the deduping probably is. The fact that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @petebacondarwin if we remove this
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const workerToWorkerEntrypointNamesMap = | ||||||||||||||||||||||||||||||||||||||||||
getWorkerToWorkerEntrypointNamesMap(userWorkers); | ||||||||||||||||||||||||||||||||||||||||||
const workerToDurableObjectClassNamesMap = | ||||||||||||||||||||||||||||||||||||||||||
|
@@ -375,6 +405,7 @@ export function getDevMiniflareOptions( | |||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||
workers: [ | ||||||||||||||||||||||||||||||||||||||||||
...assetWorkers, | ||||||||||||||||||||||||||||||||||||||||||
...externalWorkersMap.values(), | ||||||||||||||||||||||||||||||||||||||||||
...userWorkers.map((workerOptions) => { | ||||||||||||||||||||||||||||||||||||||||||
const wrappers = [ | ||||||||||||||||||||||||||||||||||||||||||
`import { createWorkerEntrypointWrapper, createDurableObjectWrapper, createWorkflowEntrypointWrapper } from '${RUNNER_PATH}';`, | ||||||||||||||||||||||||||||||||||||||||||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This E2E test causes a CLI prompt to appear for the user to choose a CF account to use for the Workers AI API - I'm not sure if there's a way to avoid that interactivity in tests, and how to programmatically set which account to use for CI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally, Wrangler, we provide a CLOUDFLARE_ACCOUNT_ID env variable to avoid such login popups, but so far I don't think we've needed that in the Vite plugin work, since it has all been local only.
I think this might be one of the first wrappedBindings that has a test?
I need to check how one can set the account ID for our plugin (if at all) right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the built types for options the plugin takes in, I think there might not be a way to set an account id dynamically.
Considering that, to avoid scope creep in the PR, I skipped the test for now with a comment: b4d16b5
Happy to go a different route though, or even just remove the test or example :)