From c32f514ca40e8b13dc9e86fdc76577b9adeb70f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Tue, 2 May 2023 17:11:14 +0100 Subject: [PATCH] [wrangler] feat: Support for Constellation AI (#3091) * [wrangler] feat: Support for Constellation AI Added initial commands to manage Constallation AI projects/models from Wrangler. * test: disable colors on tables when testing --------- Co-authored-by: Pete Bacon Darwin --- .changeset/tall-geckos-try.md | 5 + .../src/__tests__/constellation.test.ts | 371 ++++++++++++++++++ packages/wrangler/src/__tests__/index.test.ts | 2 + .../src/__tests__/mtls-certificates.test.ts | 1 + .../wrangler/src/__tests__/publish.test.ts | 4 +- packages/wrangler/src/__tests__/user.test.ts | 2 +- .../src/constellation/createProject.tsx | 51 +++ .../src/constellation/deleteProject.ts | 51 +++ .../src/constellation/deleteProjectModel.ts | 68 ++++ packages/wrangler/src/constellation/index.ts | 75 ++++ .../src/constellation/listCatalog.tsx | 35 ++ .../wrangler/src/constellation/listModel.tsx | 41 ++ .../src/constellation/listProject.tsx | 28 ++ .../src/constellation/listRuntime.tsx | 28 ++ .../wrangler/src/constellation/options.ts | 17 + packages/wrangler/src/constellation/types.ts | 17 + .../src/constellation/uploadModel.tsx | 64 +++ packages/wrangler/src/constellation/utils.ts | 90 +++++ .../src/environment-variables/factory.ts | 3 +- packages/wrangler/src/index.ts | 10 + packages/wrangler/src/logger.ts | 6 +- packages/wrangler/src/user/user.ts | 1 + 22 files changed, 965 insertions(+), 5 deletions(-) create mode 100644 .changeset/tall-geckos-try.md create mode 100644 packages/wrangler/src/__tests__/constellation.test.ts create mode 100644 packages/wrangler/src/constellation/createProject.tsx create mode 100644 packages/wrangler/src/constellation/deleteProject.ts create mode 100644 packages/wrangler/src/constellation/deleteProjectModel.ts create mode 100644 packages/wrangler/src/constellation/index.ts create mode 100644 packages/wrangler/src/constellation/listCatalog.tsx create mode 100644 packages/wrangler/src/constellation/listModel.tsx create mode 100644 packages/wrangler/src/constellation/listProject.tsx create mode 100644 packages/wrangler/src/constellation/listRuntime.tsx create mode 100644 packages/wrangler/src/constellation/options.ts create mode 100644 packages/wrangler/src/constellation/types.ts create mode 100644 packages/wrangler/src/constellation/uploadModel.tsx create mode 100644 packages/wrangler/src/constellation/utils.ts diff --git a/.changeset/tall-geckos-try.md b/.changeset/tall-geckos-try.md new file mode 100644 index 000000000000..17ae0d2c14a0 --- /dev/null +++ b/.changeset/tall-geckos-try.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Added initial commands for integrating with Constellation AI. diff --git a/packages/wrangler/src/__tests__/constellation.test.ts b/packages/wrangler/src/__tests__/constellation.test.ts new file mode 100644 index 000000000000..8c71b2cd6043 --- /dev/null +++ b/packages/wrangler/src/__tests__/constellation.test.ts @@ -0,0 +1,371 @@ +import * as fs from "node:fs"; +import { rest } from "msw"; +import { endEventLoop } from "./helpers/end-event-loop"; +import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; +import { mockConsoleMethods } from "./helpers/mock-console"; +import { clearDialogs, mockConfirm } from "./helpers/mock-dialogs"; +import { useMockIsTTY } from "./helpers/mock-istty"; +import { createFetchResult, msw } from "./helpers/msw"; +import { runInTempDir } from "./helpers/run-in-tmp"; +import { runWrangler } from "./helpers/run-wrangler"; + +describe("constellation help", () => { + const std = mockConsoleMethods(); + runInTempDir(); + + it("should show help when no argument is passed", async () => { + await runWrangler("constellation"); + await endEventLoop(); + + expect(std.out).toMatchInlineSnapshot(` + "wrangler constellation + + šŸ¤– Interact with Constellation AI models + + Commands: + wrangler constellation project Manage your projects + wrangler constellation model Manage your models + wrangler constellation catalog Check the curated model catalog + wrangler constellation runtime Check the suported runtimes + + Flags: + -j, --experimental-json-config Experimental: Support wrangler.json [boolean] + -c, --config Path to .toml configuration file [string] + -e, --env Environment to use for operations and .env files [string] + -h, --help Show help [boolean] + -v, --version Show version number [boolean]" + `); + }); + + it("should show help when an invalid argument is passed", async () => { + await expect(() => runWrangler("constellation asdf")).rejects.toThrow( + "Unknown argument: asdf" + ); + + expect(std.err).toMatchInlineSnapshot(` + "X [ERROR] Unknown argument: asdf + + " + `); + expect(std.out).toMatchInlineSnapshot(` + " + wrangler constellation + + šŸ¤– Interact with Constellation AI models + + Commands: + wrangler constellation project Manage your projects + wrangler constellation model Manage your models + wrangler constellation catalog Check the curated model catalog + wrangler constellation runtime Check the suported runtimes + + Flags: + -j, --experimental-json-config Experimental: Support wrangler.json [boolean] + -c, --config Path to .toml configuration file [string] + -e, --env Environment to use for operations and .env files [string] + -h, --help Show help [boolean] + -v, --version Show version number [boolean]" + `); + }); +}); + +describe("constellation commands", () => { + mockAccountId(); + mockApiToken(); + runInTempDir(); + const { setIsTTY } = useMockIsTTY(); + + const std = mockConsoleMethods(); + + beforeEach(() => { + // @ts-expect-error we're using a very simple setTimeout mock here + jest.spyOn(global, "setTimeout").mockImplementation((fn, _period) => { + setImmediate(fn); + }); + setIsTTY(true); + }); + + afterEach(() => { + clearDialogs(); + }); + + it("should handle project creation", async () => { + mockConstellationRequest(); + await runWrangler("constellation project create new_project ONNX"); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + āœ… Successfully created Project \\"new_project3\\"!" + `); + }); + + it("should handle project listing", async () => { + mockConstellationRequest(); + await runWrangler("constellation project list"); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ id ā”‚ name ā”‚ runtime ā”‚ created_at ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ 4806cdcf-9aa7-4fa2-b6a1-77fe9e196680 ā”‚ new_project3 ā”‚ ONNX ā”‚ 2023-04-28T13:25:58.513105Z ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜" + `); + }); + + it("should handle project deletion", async () => { + mockConstellationRequest(); + mockConfirm({ + text: "Ok to proceed?", + result: true, + }); + + await runWrangler("constellation project delete new_project3"); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + About to delete Project 'new_project3' (4806cdcf-9aa7-4fa2-b6a1-77fe9e196680). + Deleting... + Deleted 'new_project3' successfully." + `); + }); + + it("should handle catalog list", async () => { + mockConstellationRequest(); + await runWrangler("constellation catalog list"); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ project_id ā”‚ project_name ā”‚ project_runtime ā”‚ models ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ b162a29d-0a6d-4155-bedf-54a01fc8d0ef ā”‚ image-classification ā”‚ ONNX ā”‚ squeezenet1_1 ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜" + `); + }); + + it("should handle runtime list", async () => { + mockConstellationRequest(); + await runWrangler("constellation runtime list"); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ name ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ ONNX ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ XGBoost ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜" + `); + }); + + it("should handle model upload", async () => { + mockConstellationRequest(); + await fs.promises.writeFile("model.onnx", `model`); + await runWrangler( + "constellation model upload new_project3 model2 model.onnx" + ); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + āœ… Successfully uploaded Model \\"model2\\"!" + `); + }); + + it("should handle model list", async () => { + mockConstellationRequest(); + await runWrangler("constellation model list new_project3"); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ id ā”‚ project_id ā”‚ name ā”‚ description ā”‚ created_at ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ 450bb086-3c09-4991-a0cc-eed48c504ae0 ā”‚ 9d478427-dea6-4988-9b16-f6f8888d974c ā”‚ model1 ā”‚ ā”‚ 2023-04-28T11:15:14.806217Z ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ 2dd35b4e-0c7a-4c7a-a9e2-e33c0e17bc02 ā”‚ 9d478427-dea6-4988-9b16-f6f8888d974c ā”‚ model2 ā”‚ ā”‚ 2023-04-28T13:50:37.494090Z ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜" + `); + }); + + it("should handle model deletion", async () => { + mockConstellationRequest(); + mockConfirm({ + text: "Ok to proceed?", + result: true, + }); + + await runWrangler("constellation model delete new_project3 model2"); + expect(std.out).toMatchInlineSnapshot(` + "-------------------- + šŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic + šŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose + šŸš§ To give feedback, visit https://discord.gg/cloudflaredev + -------------------- + + About to delete Model 'model2' (2dd35b4e-0c7a-4c7a-a9e2-e33c0e17bc02). + Deleting... + Deleted 'model2' successfully." + `); + }); +}); + +/** Create a mock handler for Constellation API */ +function mockConstellationRequest() { + msw.use( + rest.get("*/accounts/:accountId/constellation/project", (req, res, ctx) => { + return res.once( + ctx.json( + createFetchResult( + [ + { + id: "4806cdcf-9aa7-4fa2-b6a1-77fe9e196680", + name: "new_project3", + runtime: "ONNX", + created_at: "2023-04-28T13:25:58.513105Z", + }, + ], + true + ) + ) + ); + }), + rest.post( + "*/accounts/:accountId/constellation/project", + (req, res, ctx) => { + return res.once( + ctx.json( + createFetchResult( + { + id: "4806cdcf-9aa7-4fa2-b6a1-77fe9e196680", + name: "new_project3", + runtime: "ONNX", + created_at: "2023-04-28T13:25:58.513105Z", + }, + true + ) + ) + ); + } + ), + rest.delete( + "*/accounts/:accountId/constellation/project/4806cdcf-9aa7-4fa2-b6a1-77fe9e196680", + (req, res, ctx) => { + return res.once(ctx.json(createFetchResult(null, true))); + } + ), + rest.get("*/accounts/:accountId/constellation/catalog", (req, res, ctx) => { + return res.once( + ctx.json( + createFetchResult( + [ + { + project: { + id: "b162a29d-0a6d-4155-bedf-54a01fc8d0ef", + name: "image-classification", + runtime: "ONNX", + created_at: "2023-04-27T18:55:38.417187Z", + }, + models: [ + { + id: "edb202d3-f4ac-43ab-8762-3ae6b43c4c57", + project_id: "b162a29d-0a6d-4155-bedf-54a01fc8d0ef", + name: "squeezenet1_1", + description: null, + created_at: "2023-04-27T18:56:15.305087Z", + }, + ], + }, + ], + true + ) + ) + ); + }), + rest.get("*/accounts/:accountId/constellation/runtime", (req, res, ctx) => { + return res.once(ctx.json(createFetchResult(["ONNX", "XGBoost"], true))); + }), + rest.post( + "*/accounts/:accountId/constellation/project/4806cdcf-9aa7-4fa2-b6a1-77fe9e196680/model", + (req, res, ctx) => { + return res.once( + ctx.json( + createFetchResult( + { + id: "2dd35b4e-0c7a-4c7a-a9e2-e33c0e17bc02", + project_id: "4806cdcf-9aa7-4fa2-b6a1-77fe9e196680", + name: "model2", + description: null, + created_at: "2023-04-28T13:50:37.494090Z", + }, + true + ) + ) + ); + } + ), + rest.get( + "*/accounts/:accountId/constellation/project/4806cdcf-9aa7-4fa2-b6a1-77fe9e196680/model", + (req, res, ctx) => { + return res.once( + ctx.json( + createFetchResult( + [ + { + id: "450bb086-3c09-4991-a0cc-eed48c504ae0", + project_id: "9d478427-dea6-4988-9b16-f6f8888d974c", + name: "model1", + description: null, + created_at: "2023-04-28T11:15:14.806217Z", + }, + { + id: "2dd35b4e-0c7a-4c7a-a9e2-e33c0e17bc02", + project_id: "9d478427-dea6-4988-9b16-f6f8888d974c", + name: "model2", + description: null, + created_at: "2023-04-28T13:50:37.494090Z", + }, + ], + true + ) + ) + ); + } + ), + rest.delete( + "*/accounts/:accountId/constellation/project/4806cdcf-9aa7-4fa2-b6a1-77fe9e196680/model/2dd35b4e-0c7a-4c7a-a9e2-e33c0e17bc02", + (req, res, ctx) => { + return res.once(ctx.json(createFetchResult(null, true))); + } + ) + ); +} diff --git a/packages/wrangler/src/__tests__/index.test.ts b/packages/wrangler/src/__tests__/index.test.ts index 0f3807385293..51985eaaa37b 100644 --- a/packages/wrangler/src/__tests__/index.test.ts +++ b/packages/wrangler/src/__tests__/index.test.ts @@ -49,6 +49,7 @@ describe("wrangler", () => { wrangler r2 šŸ“¦ Interact with an R2 store wrangler dispatch-namespace šŸ“¦ Interact with a dispatch namespace wrangler d1 šŸ—„ Interact with a D1 database + wrangler constellation šŸ¤– Interact with Constellation AI models wrangler pubsub šŸ“® Interact and manage Pub/Sub Brokers wrangler mtls-certificate šŸŖŖ Manage certificates used for mTLS connections wrangler login šŸ”“ Login to Cloudflare @@ -102,6 +103,7 @@ describe("wrangler", () => { wrangler r2 šŸ“¦ Interact with an R2 store wrangler dispatch-namespace šŸ“¦ Interact with a dispatch namespace wrangler d1 šŸ—„ Interact with a D1 database + wrangler constellation šŸ¤– Interact with Constellation AI models wrangler pubsub šŸ“® Interact and manage Pub/Sub Brokers wrangler mtls-certificate šŸŖŖ Manage certificates used for mTLS connections wrangler login šŸ”“ Login to Cloudflare diff --git a/packages/wrangler/src/__tests__/mtls-certificates.test.ts b/packages/wrangler/src/__tests__/mtls-certificates.test.ts index bd61509d5e7c..81cb974531ae 100644 --- a/packages/wrangler/src/__tests__/mtls-certificates.test.ts +++ b/packages/wrangler/src/__tests__/mtls-certificates.test.ts @@ -379,6 +379,7 @@ describe("wrangler", () => { wrangler r2 šŸ“¦ Interact with an R2 store wrangler dispatch-namespace šŸ“¦ Interact with a dispatch namespace wrangler d1 šŸ—„ Interact with a D1 database + wrangler constellation šŸ¤– Interact with Constellation AI models wrangler pubsub šŸ“® Interact and manage Pub/Sub Brokers wrangler mtls-certificate šŸŖŖ Manage certificates used for mTLS connections wrangler login šŸ”“ Login to Cloudflare diff --git a/packages/wrangler/src/__tests__/publish.test.ts b/packages/wrangler/src/__tests__/publish.test.ts index 5886ce7771d2..04b4f0438dd5 100644 --- a/packages/wrangler/src/__tests__/publish.test.ts +++ b/packages/wrangler/src/__tests__/publish.test.ts @@ -140,7 +140,7 @@ describe("publish", () => { expect(std.out).toMatchInlineSnapshot(` "Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20constellation%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in. Total Upload: xx KiB / gzip: xx KiB Uploaded test-name (TIMINGS) @@ -180,7 +180,7 @@ describe("publish", () => { expect(std.out).toMatchInlineSnapshot(` "Attempting to login via OAuth... - Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20constellation%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in. Total Upload: xx KiB / gzip: xx KiB Uploaded test-name (TIMINGS) diff --git a/packages/wrangler/src/__tests__/user.test.ts b/packages/wrangler/src/__tests__/user.test.ts index 740bb9b8dfbe..15efbc682689 100644 --- a/packages/wrangler/src/__tests__/user.test.ts +++ b/packages/wrangler/src/__tests__/user.test.ts @@ -60,7 +60,7 @@ describe("User", () => { expect(counter).toBe(1); expect(std.out).toMatchInlineSnapshot(` "Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20constellation%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ diff --git a/packages/wrangler/src/constellation/createProject.tsx b/packages/wrangler/src/constellation/createProject.tsx new file mode 100644 index 000000000000..e60588abfed7 --- /dev/null +++ b/packages/wrangler/src/constellation/createProject.tsx @@ -0,0 +1,51 @@ +import { fetchResult } from "../cfetch"; +import { withConfig } from "../config"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { takeName } from "./options"; +import { constellationBetaWarning } from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; +import type { Project } from "./types"; + +export function options(yargs: CommonYargsArgv) { + return takeName(yargs) + .positional("runtime", { + describe: "The name of the runtime to use", + type: "string", + demandOption: true, + }) + .epilogue(constellationBetaWarning); +} + +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ name, runtime, config }): Promise => { + const accountId = await requireAuth(config); + + logger.log(constellationBetaWarning); + + let proj: Project; + try { + proj = await fetchResult(`/accounts/${accountId}/constellation/project`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + runtime, + }), + }); + } catch (e) { + if ((e as { code: number }).code === 7409) { + throw new Error("A project with that name already exists"); + } + throw e; + } + + logger.log(`āœ… Successfully created Project "${proj.name}"!`); + } +); diff --git a/packages/wrangler/src/constellation/deleteProject.ts b/packages/wrangler/src/constellation/deleteProject.ts new file mode 100644 index 000000000000..8fdbedf8ba4e --- /dev/null +++ b/packages/wrangler/src/constellation/deleteProject.ts @@ -0,0 +1,51 @@ +import { fetchResult } from "../cfetch"; +import { withConfig } from "../config"; +import { confirm } from "../dialogs"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { takeName } from "./options"; +import { constellationBetaWarning, getProjectByName } from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; + +export function options(yargs: CommonYargsArgv) { + return takeName(yargs) + .option("force", { + describe: "Skip confirmation", + type: "boolean", + alias: "f", + default: false, + }) + .epilogue(constellationBetaWarning); +} +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ name, force, config }): Promise => { + const accountId = await requireAuth(config); + logger.log(constellationBetaWarning); + + const proj = await getProjectByName(config, accountId, name); + + logger.log(`About to delete Project '${name}' (${proj.id}).`); + if (!force) { + const response = await confirm(`Ok to proceed?`); + if (!response) { + logger.log(`Not deleting.`); + return; + } + + logger.log("Deleting..."); + } + + await fetchResult( + `/accounts/${accountId}/constellation/project/${proj.id}`, + { + method: "DELETE", + } + ); + + logger.log(`Deleted '${name}' successfully.`); + } +); diff --git a/packages/wrangler/src/constellation/deleteProjectModel.ts b/packages/wrangler/src/constellation/deleteProjectModel.ts new file mode 100644 index 000000000000..948de8f2a7f9 --- /dev/null +++ b/packages/wrangler/src/constellation/deleteProjectModel.ts @@ -0,0 +1,68 @@ +import { fetchResult } from "../cfetch"; +import { withConfig } from "../config"; +import { confirm } from "../dialogs"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { takeName } from "./options"; +import { + constellationBetaWarning, + getProjectByName, + getProjectModelByName, +} from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; +import type { Project, Model } from "./types"; + +export function options(yargs: CommonYargsArgv) { + return takeName(yargs) + .positional("modelName", { + describe: "The name of the uploaded model", + type: "string", + demandOption: true, + }) + .option("force", { + describe: "Skip confirmation", + type: "boolean", + alias: "f", + default: false, + }) + .epilogue(constellationBetaWarning); +} +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ name, modelName, force, config }): Promise => { + const accountId = await requireAuth(config); + logger.log(constellationBetaWarning); + + const proj: Project = await getProjectByName(config, accountId, name); + + const model: Model = await getProjectModelByName( + config, + accountId, + proj, + modelName + ); + + logger.log(`About to delete Model '${modelName}' (${model.id}).`); + if (!force) { + const response = await confirm(`Ok to proceed?`); + if (!response) { + logger.log(`Not deleting.`); + return; + } + + logger.log("Deleting..."); + } + + await fetchResult( + `/accounts/${accountId}/constellation/project/${proj.id}/model/${model.id}`, + { + method: "DELETE", + } + ); + + logger.log(`Deleted '${modelName}' successfully.`); + } +); diff --git a/packages/wrangler/src/constellation/index.ts b/packages/wrangler/src/constellation/index.ts new file mode 100644 index 000000000000..dcd4a8b6fb9a --- /dev/null +++ b/packages/wrangler/src/constellation/index.ts @@ -0,0 +1,75 @@ +import * as CreateProject from "./createProject"; +import * as DeleteProject from "./deleteProject"; +import * as DeleteProjectModel from "./deleteProjectModel"; +import * as ListCatalog from "./listCatalog"; +import * as ListModel from "./listModel"; +import * as ListProject from "./listProject"; +import * as ListRuntime from "./listRuntime"; +import * as UploadModel from "./uploadModel"; +import type { CommonYargsArgv } from "../yargs-types"; + +export function constellation(yargs: CommonYargsArgv) { + return yargs + .command("project", "Manage your projects", (constProjYargs) => { + return constProjYargs + .command( + "list", + "List your projects", + ListProject.options, + ListProject.handler + ) + .command( + "create ", + "Create project", + CreateProject.options, + CreateProject.handler + ) + .command( + "delete ", + "Delete project", + DeleteProject.options, + DeleteProject.handler + ); + }) + .command("model", "Manage your models", (constModelYargs) => { + return constModelYargs + .command( + "upload ", + "Upload a model for an existing project", + UploadModel.options, + UploadModel.handler + ) + .command( + "list ", + "List models of a project", + ListModel.options, + ListModel.handler + ) + .command( + "delete ", + "Delete a model of a project", + DeleteProjectModel.options, + DeleteProjectModel.handler + ); + }) + .command( + "catalog", + "Check the curated model catalog", + (constCatalogYargs) => { + return constCatalogYargs.command( + "list", + "List catalog models", + ListCatalog.options, + ListCatalog.handler + ); + } + ) + .command("runtime", "Check the suported runtimes", (constRuntimeYargs) => { + return constRuntimeYargs.command( + "list", + "List suported runtimes", + ListRuntime.options, + ListRuntime.handler + ); + }); +} diff --git a/packages/wrangler/src/constellation/listCatalog.tsx b/packages/wrangler/src/constellation/listCatalog.tsx new file mode 100644 index 000000000000..9ad72d11ea9a --- /dev/null +++ b/packages/wrangler/src/constellation/listCatalog.tsx @@ -0,0 +1,35 @@ +import { withConfig } from "../config"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { asJson } from "./options"; +import { constellationBetaWarning, listCatalogEntries } from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; + +export function options(yargs: CommonYargsArgv) { + return asJson(yargs).epilogue(constellationBetaWarning); +} + +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ json, config }): Promise => { + const accountId = await requireAuth(config); + const entries = await listCatalogEntries(accountId); + + if (json) { + logger.log(JSON.stringify(entries, null, 2)); + } else { + logger.log(constellationBetaWarning); + logger.table( + entries.map((entry) => ({ + project_id: entry.project.id, + project_name: entry.project.name, + project_runtime: entry.project.runtime, + models: entry.models.map((model) => model.name).join(","), + })) + ); + } + } +); diff --git a/packages/wrangler/src/constellation/listModel.tsx b/packages/wrangler/src/constellation/listModel.tsx new file mode 100644 index 000000000000..2214fe59a54d --- /dev/null +++ b/packages/wrangler/src/constellation/listModel.tsx @@ -0,0 +1,41 @@ +import { withConfig } from "../config"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { takeName } from "./options"; +import { + constellationBetaWarning, + getProjectByName, + listModels, +} from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; +import type { Project } from "./types"; + +export function options(yargs: CommonYargsArgv) { + return takeName(yargs) + .option("json", { + describe: "return output as clean JSON", + type: "boolean", + default: false, + }) + .epilogue(constellationBetaWarning); +} + +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ name, json, config }): Promise => { + const accountId = await requireAuth(config); + const proj: Project = await getProjectByName(config, accountId, name); + + const models = await listModels(accountId, proj); + + if (json) { + logger.log(JSON.stringify(models, null, 2)); + } else { + logger.log(constellationBetaWarning); + logger.table(models); + } + } +); diff --git a/packages/wrangler/src/constellation/listProject.tsx b/packages/wrangler/src/constellation/listProject.tsx new file mode 100644 index 000000000000..d5f216abc563 --- /dev/null +++ b/packages/wrangler/src/constellation/listProject.tsx @@ -0,0 +1,28 @@ +import { withConfig } from "../config"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { asJson } from "./options"; +import { constellationBetaWarning, listProjects } from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; + +export function options(yargs: CommonYargsArgv) { + return asJson(yargs).epilogue(constellationBetaWarning); +} + +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ json, config }): Promise => { + const accountId = await requireAuth(config); + const projs = await listProjects(accountId); + + if (json) { + logger.log(JSON.stringify(projs, null, 2)); + } else { + logger.log(constellationBetaWarning); + logger.table(projs); + } + } +); diff --git a/packages/wrangler/src/constellation/listRuntime.tsx b/packages/wrangler/src/constellation/listRuntime.tsx new file mode 100644 index 000000000000..1be3a07a177f --- /dev/null +++ b/packages/wrangler/src/constellation/listRuntime.tsx @@ -0,0 +1,28 @@ +import { withConfig } from "../config"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { asJson } from "./options"; +import { constellationBetaWarning, listRuntimes } from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; + +export function options(yargs: CommonYargsArgv) { + return asJson(yargs).epilogue(constellationBetaWarning); +} + +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ json, config }): Promise => { + const accountId = await requireAuth(config); + const runtimes = await listRuntimes(accountId); + + if (json) { + logger.log(JSON.stringify(runtimes, null, 2)); + } else { + logger.log(constellationBetaWarning); + logger.table(runtimes.map((runtime) => ({ name: runtime }))); + } + } +); diff --git a/packages/wrangler/src/constellation/options.ts b/packages/wrangler/src/constellation/options.ts new file mode 100644 index 000000000000..f97625fb5e45 --- /dev/null +++ b/packages/wrangler/src/constellation/options.ts @@ -0,0 +1,17 @@ +import type { CommonYargsArgv } from "../yargs-types"; + +export function takeName(yargs: CommonYargsArgv) { + return yargs.positional("name", { + describe: "The name of the project", + type: "string", + demandOption: true, + }); +} + +export function asJson(yargs: CommonYargsArgv) { + return yargs.option("json", { + describe: "return output as clean JSON", + type: "boolean", + default: false, + }); +} diff --git a/packages/wrangler/src/constellation/types.ts b/packages/wrangler/src/constellation/types.ts new file mode 100644 index 000000000000..bfc265a19b30 --- /dev/null +++ b/packages/wrangler/src/constellation/types.ts @@ -0,0 +1,17 @@ +export type Project = { + id: string; + name: string; + runtime: string; +}; + +export type Model = { + id: string; + project_id: string; + name: string; + description: string; +}; + +export type CatalogEntry = { + project: Project; + models: Model[]; +}; diff --git a/packages/wrangler/src/constellation/uploadModel.tsx b/packages/wrangler/src/constellation/uploadModel.tsx new file mode 100644 index 000000000000..6b1808650dda --- /dev/null +++ b/packages/wrangler/src/constellation/uploadModel.tsx @@ -0,0 +1,64 @@ +import { readFileSync } from "node:fs"; +import { FormData, File } from "undici"; +import { fetchResult } from "../cfetch"; +import { withConfig } from "../config"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { takeName } from "./options"; +import { constellationBetaWarning, getProjectByName } from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; +import type { Model } from "./types"; + +export function options(yargs: CommonYargsArgv) { + return takeName(yargs) + .positional("modelName", { + describe: "The name of the uploaded model", + type: "string", + demandOption: true, + }) + .positional("modelFile", { + describe: "The name of the local file with the model contents", + type: "string", + demandOption: true, + }) + .epilogue(constellationBetaWarning); +} +type HandlerOptions = StrictYargsOptionsToInterface; +export const handler = withConfig( + async ({ name, modelName, modelFile, config }): Promise => { + const accountId = await requireAuth(config); + logger.log(constellationBetaWarning); + + const proj = await getProjectByName(config, accountId, name); + + const formData = new FormData(); + formData.set( + "file", + new File([readFileSync(modelFile)], modelFile, { + type: "application/octet-stream", + }) + ); + formData.set("name", modelName); + + let model: Model; + try { + model = await fetchResult( + `/accounts/${accountId}/constellation/project/${proj.id}/model`, + { + method: "POST", + body: formData, + } + ); + } catch (e) { + if ((e as { code: number }).code === 7408) { + throw new Error("A model with that name already exists"); + } + throw e; + } + + logger.log(`āœ… Successfully uploaded Model "${model.name}"!`); + } +); diff --git a/packages/wrangler/src/constellation/utils.ts b/packages/wrangler/src/constellation/utils.ts new file mode 100644 index 000000000000..b49c21d14fba --- /dev/null +++ b/packages/wrangler/src/constellation/utils.ts @@ -0,0 +1,90 @@ +import { fetchResult } from "../cfetch"; +import { getEnvironmentVariableFactory } from "../environment-variables/factory"; +import type { Config } from "../config"; +import type { Project, Model, CatalogEntry } from "./types"; + +export const getConstellationWarningFromEnv = getEnvironmentVariableFactory({ + variableName: "NO_CONSTELLATION_WARNING", +}); + +export const constellationBetaWarning = + getConstellationWarningFromEnv() !== undefined + ? "" + : "--------------------\nšŸš§ Constellation AI is currently in open alpha and is not recommended for production data and traffic\nšŸš§ Please report any bugs to https://github.com/cloudflare/workers-sdk/issues/new/choose\nšŸš§ To give feedback, visit https://discord.gg/cloudflaredev\n--------------------\n"; + +export const getProjectByName = async ( + config: Config, + accountId: string, + name: string +): Promise => { + const allProjects = await listProjects(accountId); + const matchingProj = allProjects.find((proj) => proj.name === name); + if (!matchingProj) { + throw new Error(`Couldn't find Project with name '${name}'`); + } + return matchingProj; +}; + +export const getProjectModelByName = async ( + config: Config, + accountId: string, + proj: Project, + modelName: string +): Promise => { + const allModels = await listModels(accountId, proj); + const matchingModel = allModels.find((model) => model.name === modelName); + if (!matchingModel) { + throw new Error(`Couldn't find Model with name '${modelName}'`); + } + return matchingModel; +}; + +export async function constellationList( + accountId: string, + partialUrl: string +): Promise> { + const pageSize = 50; + let page = 1; + const results = []; + while (results.length % pageSize === 0) { + const json: Array = await fetchResult( + `/accounts/${accountId}/constellation/${partialUrl}`, + {}, + new URLSearchParams({ + per_page: pageSize.toString(), + page: page.toString(), + }) + ); + page++; + results.push(...json); + if (json.length < pageSize) { + break; + } + } + return results; +} + +export const listCatalogEntries = async ( + accountId: string +): Promise> => { + return await constellationList(accountId, "catalog"); +}; + +export const listModels = async ( + accountId: string, + proj: Project +): Promise> => { + return constellationList(accountId, `project/${proj.id}/model`); +}; + +export const listProjects = async ( + accountId: string +): Promise> => { + return await constellationList(accountId, "project"); +}; + +export const listRuntimes = async ( + accountId: string +): Promise> => { + return await constellationList(accountId, "runtime"); +}; diff --git a/packages/wrangler/src/environment-variables/factory.ts b/packages/wrangler/src/environment-variables/factory.ts index 62c9e4cbe97c..7b656b4f6714 100644 --- a/packages/wrangler/src/environment-variables/factory.ts +++ b/packages/wrangler/src/environment-variables/factory.ts @@ -14,7 +14,8 @@ type VariableNames = | "WRANGLER_AUTH_URL" | "WRANGLER_TOKEN_URL" | "WRANGLER_REVOKE_URL" - | "WRANGLER_CF_AUTHORIZATION_TOKEN"; + | "WRANGLER_CF_AUTHORIZATION_TOKEN" + | "NO_CONSTELLATION_WARNING"; type DeprecatedNames = | "CF_ACCOUNT_ID" diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 32e34b6d8bba..53e9981719e2 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -7,6 +7,7 @@ import makeCLI from "yargs"; import { version as wranglerVersion } from "../package.json"; import { isBuildFailure } from "./bundle"; import { loadDotEnv, readConfig } from "./config"; +import { constellation } from "./constellation"; import { d1 } from "./d1"; import { deleteHandler, deleteOptions } from "./delete"; import { @@ -431,6 +432,15 @@ export function createCLIParser(argv: string[]) { return d1(d1Yargs.command(subHelp)); }); + // ai + wrangler.command( + "constellation", + "šŸ¤– Interact with Constellation AI models", + (aiYargs) => { + return constellation(aiYargs.command(subHelp)); + } + ); + // pubsub wrangler.command( "pubsub", diff --git a/packages/wrangler/src/logger.ts b/packages/wrangler/src/logger.ts index 5f7ebab6a303..a9f110a48346 100644 --- a/packages/wrangler/src/logger.ts +++ b/packages/wrangler/src/logger.ts @@ -61,7 +61,11 @@ export class Logger { const keys: Keys[] = data.length === 0 ? [] : (Object.keys(data[0]) as Keys[]); const t = new CLITable({ - head: keys.map((k) => chalk.bold.blue(k)), + head: keys, + style: { + head: chalk.level ? ["blue"] : [], + border: chalk.level ? ["gray"] : [], + }, }); t.push(...data.map((row) => keys.map((k) => row[k]))); return this.doLog("log", [t.toString()]); diff --git a/packages/wrangler/src/user/user.ts b/packages/wrangler/src/user/user.ts index ccb8f58af52a..1553558100bd 100644 --- a/packages/wrangler/src/user/user.ts +++ b/packages/wrangler/src/user/user.ts @@ -347,6 +347,7 @@ const Scopes = { "See and change Cloudflare Pages projects, settings and deployments.", "zone:read": "Grants read level access to account zone.", "ssl_certs:write": "See and manage mTLS certificates for your account", + "constellation:write": "Manage Constellation AI projects/models", } as const; /**