Skip to content

Commit 3556474

Browse files
authored
wrangler: Add AI binding (#3992)
* wrangler: Add AI binding Added binding for the AI project. * Workers AI: added example
1 parent 7ba16cd commit 3556474

14 files changed

+220
-0
lines changed

.changeset/brave-stingrays-shout.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Add AI binding that will be used to interact with the AI project.
6+
7+
Example `wrangler.toml`
8+
9+
name = "ai-worker"
10+
main = "src/index.ts"
11+
12+
[ai]
13+
binding = "AI"
14+
15+
Example script:
16+
17+
import Ai from "@cloudflare/ai"
18+
19+
export default {
20+
async fetch(request: Request, env: Env): Promise<Response> {
21+
const ai = new Ai(env.AI);
22+
23+
const story = await ai.run({
24+
model: 'llama-2',
25+
input: {
26+
prompt: 'Tell me a story about the future of the Cloudflare dev platform'
27+
}
28+
});
29+
30+
return new Response(JSON.stringify(story));
31+
},
32+
};
33+
34+
export interface Env {
35+
AI: any;
36+
}

packages/wrangler/src/__tests__/configuration.test.ts

+59
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe("normalizeAndValidateConfig()", () => {
6464
site: undefined,
6565
text_blobs: undefined,
6666
browser: undefined,
67+
ai: undefined,
6768
triggers: {
6869
crons: [],
6970
},
@@ -1620,6 +1621,64 @@ describe("normalizeAndValidateConfig()", () => {
16201621
});
16211622
});
16221623

1624+
describe("[ai]", () => {
1625+
it("should error if ai is an array", () => {
1626+
const { diagnostics } = normalizeAndValidateConfig(
1627+
{ ai: [] } as unknown as RawConfig,
1628+
undefined,
1629+
{ env: undefined }
1630+
);
1631+
1632+
expect(diagnostics.hasWarnings()).toBe(false);
1633+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
1634+
"Processing wrangler configuration:
1635+
- The field \\"ai\\" should be an object but got []."
1636+
`);
1637+
});
1638+
1639+
it("should error if ai is a string", () => {
1640+
const { diagnostics } = normalizeAndValidateConfig(
1641+
{ ai: "BAD" } as unknown as RawConfig,
1642+
undefined,
1643+
{ env: undefined }
1644+
);
1645+
1646+
expect(diagnostics.hasWarnings()).toBe(false);
1647+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
1648+
"Processing wrangler configuration:
1649+
- The field \\"ai\\" should be an object but got \\"BAD\\"."
1650+
`);
1651+
});
1652+
1653+
it("should error if ai is a number", () => {
1654+
const { diagnostics } = normalizeAndValidateConfig(
1655+
{ ai: 999 } as unknown as RawConfig,
1656+
undefined,
1657+
{ env: undefined }
1658+
);
1659+
1660+
expect(diagnostics.hasWarnings()).toBe(false);
1661+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
1662+
"Processing wrangler configuration:
1663+
- The field \\"ai\\" should be an object but got 999."
1664+
`);
1665+
});
1666+
1667+
it("should error if ai is null", () => {
1668+
const { diagnostics } = normalizeAndValidateConfig(
1669+
{ ai: null } as unknown as RawConfig,
1670+
undefined,
1671+
{ env: undefined }
1672+
);
1673+
1674+
expect(diagnostics.hasWarnings()).toBe(false);
1675+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
1676+
"Processing wrangler configuration:
1677+
- The field \\"ai\\" should be an object but got null."
1678+
`);
1679+
});
1680+
});
1681+
16231682
describe("[kv_namespaces]", () => {
16241683
it("should error if kv_namespaces is an object", () => {
16251684
const { diagnostics } = normalizeAndValidateConfig(

packages/wrangler/src/__tests__/deploy.test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -8367,6 +8367,43 @@ export default{
83678367
});
83688368
});
83698369

8370+
describe("ai", () => {
8371+
it("should upload ai bindings", async () => {
8372+
writeWranglerToml({
8373+
ai: { binding: "AI_BIND" },
8374+
browser: { binding: "MYBROWSER" },
8375+
});
8376+
await fs.promises.writeFile("index.js", `export default {};`);
8377+
mockSubDomainRequest();
8378+
mockUploadWorkerRequest({
8379+
expectedBindings: [
8380+
{
8381+
type: "browser",
8382+
name: "MYBROWSER",
8383+
},
8384+
{
8385+
type: "ai",
8386+
name: "AI_BIND",
8387+
},
8388+
],
8389+
});
8390+
8391+
await runWrangler("deploy index.js");
8392+
expect(std.out).toMatchInlineSnapshot(`
8393+
"Total Upload: xx KiB / gzip: xx KiB
8394+
Your worker has access to the following bindings:
8395+
- Browser:
8396+
- Name: MYBROWSER
8397+
- AI:
8398+
- Name: AI_BIND
8399+
Uploaded test-name (TIMINGS)
8400+
Published test-name (TIMINGS)
8401+
https://test-name.test-sub-domain.workers.dev
8402+
Current Deployment ID: Galaxy-Class"
8403+
`);
8404+
});
8405+
});
8406+
83708407
describe("mtls_certificates", () => {
83718408
it("should upload mtls_certificate bindings", async () => {
83728409
writeWranglerToml({

packages/wrangler/src/api/pages/create-worker-bundle-contents.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function createWorkerBundleFormData(workerBundle: BundleResult): FormData {
5454
wasm_modules: undefined,
5555
text_blobs: undefined,
5656
browser: undefined,
57+
ai: undefined,
5758
data_blobs: undefined,
5859
durable_objects: undefined,
5960
queues: undefined,

packages/wrangler/src/config/environment.ts

+9
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,15 @@ interface EnvironmentNonInheritable {
519519
}
520520
| undefined;
521521

522+
/**
523+
* Binding to the AI project.
524+
*/
525+
ai:
526+
| {
527+
binding: string;
528+
}
529+
| undefined;
530+
522531
/**
523532
* "Unsafe" tables for features that aren't directly supported by wrangler.
524533
*

packages/wrangler/src/config/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
106106
analytics_engine_datasets,
107107
text_blobs,
108108
browser,
109+
ai,
109110
unsafe,
110111
vars,
111112
wasm_modules,
@@ -296,6 +297,13 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
296297
});
297298
}
298299

300+
if (ai !== undefined) {
301+
output.push({
302+
type: "AI",
303+
entries: [{ key: "Name", value: ai.binding }],
304+
});
305+
}
306+
299307
if (unsafe?.bindings !== undefined && unsafe.bindings.length > 0) {
300308
output.push({
301309
type: "Unsafe",

packages/wrangler/src/config/validation-helpers.ts

+6
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,12 @@ export const getBindingNames = (value: unknown): string[] => {
571571
} else if (isNamespaceList(value)) {
572572
return value.map(({ binding }) => binding);
573573
} else if (isRecord(value)) {
574+
// browser and AI bindings are single values with a similar shape
575+
// { binding = "name" }
576+
if (value["binding"] !== undefined) {
577+
return [value["binding"] as string];
578+
}
579+
574580
return Object.keys(value).filter((k) => value[k] !== undefined);
575581
} else {
576582
return [];

packages/wrangler/src/config/validation.ts

+37
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,16 @@ function normalizeAndValidateEnvironment(
13141314
validateBrowserBinding(envName),
13151315
undefined
13161316
),
1317+
ai: notInheritable(
1318+
diagnostics,
1319+
topLevelEnv,
1320+
rawConfig,
1321+
rawEnv,
1322+
envName,
1323+
"ai",
1324+
validateAIBinding(envName),
1325+
undefined
1326+
),
13171327
zone_id: rawEnv.zone_id,
13181328
logfwdr: inheritable(
13191329
diagnostics,
@@ -1893,6 +1903,30 @@ const validateBrowserBinding =
18931903
return isValid;
18941904
};
18951905

1906+
const validateAIBinding =
1907+
(envName: string): ValidatorFn =>
1908+
(diagnostics, field, value, config) => {
1909+
const fieldPath =
1910+
config === undefined ? `${field}` : `env.${envName}.${field}`;
1911+
1912+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
1913+
diagnostics.errors.push(
1914+
`The field "${fieldPath}" should be an object but got ${JSON.stringify(
1915+
value
1916+
)}.`
1917+
);
1918+
return false;
1919+
}
1920+
1921+
let isValid = true;
1922+
if (!isRequiredProperty(value, "binding", "string")) {
1923+
diagnostics.errors.push(`binding should have a string "binding" field.`);
1924+
isValid = false;
1925+
}
1926+
1927+
return isValid;
1928+
};
1929+
18961930
/**
18971931
* Check that the given field is a valid "unsafe" binding object.
18981932
*
@@ -1920,6 +1954,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
19201954
"data_blob",
19211955
"text_blob",
19221956
"browser",
1957+
"ai",
19231958
"kv_namespace",
19241959
"durable_object_namespace",
19251960
"d1_database",
@@ -2278,6 +2313,7 @@ const validateBindingsHaveUniqueNames = (
22782313
analytics_engine_datasets,
22792314
text_blobs,
22802315
browser,
2316+
ai,
22812317
unsafe,
22822318
vars,
22832319
define,
@@ -2294,6 +2330,7 @@ const validateBindingsHaveUniqueNames = (
22942330
"Analytics Engine Dataset": getBindingNames(analytics_engine_datasets),
22952331
"Text Blob": getBindingNames(text_blobs),
22962332
Browser: getBindingNames(browser),
2333+
AI: getBindingNames(ai),
22972334
Unsafe: getBindingNames(unsafe),
22982335
"Environment Variable": getBindingNames(vars),
22992336
Definition: getBindingNames(define),

packages/wrangler/src/deploy/deploy.ts

+1
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
529529
vars: { ...config.vars, ...props.vars },
530530
wasm_modules: config.wasm_modules,
531531
browser: config.browser,
532+
ai: config.ai,
532533
text_blobs: {
533534
...config.text_blobs,
534535
...(assets.manifest &&

packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type WorkerMetadataBinding =
3535
| { type: "wasm_module"; name: string; part: string }
3636
| { type: "text_blob"; name: string; part: string }
3737
| { type: "browser"; name: string }
38+
| { type: "ai"; name: string }
3839
| { type: "data_blob"; name: string; part: string }
3940
| { type: "kv_namespace"; name: string; namespace_id: string }
4041
| {
@@ -268,6 +269,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
268269
});
269270
}
270271

272+
if (bindings.ai !== undefined) {
273+
metadataBindings.push({
274+
name: bindings.ai.binding,
275+
type: "ai",
276+
});
277+
}
278+
271279
for (const [name, filePath] of Object.entries(bindings.text_blobs || {})) {
272280
metadataBindings.push({
273281
name,

packages/wrangler/src/deployment-bundle/worker.ts

+9
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ export interface CfBrowserBinding {
103103
binding: string;
104104
}
105105

106+
/**
107+
* A binding to the AI project
108+
*/
109+
110+
export interface CfAIBinding {
111+
binding: string;
112+
}
113+
106114
/**
107115
* A binding to a data blob (in service-worker format)
108116
*/
@@ -256,6 +264,7 @@ export interface CfWorkerInit {
256264
wasm_modules: CfWasmModuleBindings | undefined;
257265
text_blobs: CfTextBlobBindings | undefined;
258266
browser: CfBrowserBinding | undefined;
267+
ai: CfAIBinding | undefined;
259268
data_blobs: CfDataBlobBindings | undefined;
260269
durable_objects: { bindings: CfDurableObject[] } | undefined;
261270
queues: CfQueue[] | undefined;

packages/wrangler/src/dev.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ function getBindings(
876876
wasm_modules: configParam.wasm_modules,
877877
text_blobs: configParam.text_blobs,
878878
browser: configParam.browser,
879+
ai: configParam.ai,
879880
data_blobs: configParam.data_blobs,
880881
durable_objects: {
881882
bindings: [

packages/wrangler/src/init.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,13 @@ export function mapBindings(bindings: WorkerMetadataBinding[]): RawConfig {
10461046
};
10471047
}
10481048
break;
1049+
case "ai":
1050+
{
1051+
configObj.ai = {
1052+
binding: binding.name,
1053+
};
1054+
}
1055+
break;
10491056
case "r2_bucket":
10501057
{
10511058
configObj.r2_buckets = [

packages/wrangler/src/secret/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export const secret = (secretYargs: CommonYargsArgv) => {
113113
analytics_engine_datasets: [],
114114
wasm_modules: {},
115115
browser: undefined,
116+
ai: undefined,
116117
text_blobs: {},
117118
data_blobs: {},
118119
dispatch_namespaces: [],

0 commit comments

Comments
 (0)