diff --git a/README.md b/README.md
index 360247b..5bb19a5 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ This means `flare-edge-cli` is not just a deploy wrapper. It is an agent-oriente
- Validates Go code against a Workers/Wasm compatibility profile
- Builds `.wasm` artifacts and the JavaScript Worker shim
- Runs local or remote dev sessions through Wrangler
+- Configures Workers AI bindings for Go-based AI Workers
- Deploys versioned Workers and manages routes, secrets, KV, D1, R2, and releases
- Tears down Workers and optional side-effect resources cleanly
@@ -235,10 +236,42 @@ Supported templates:
- `edge-http`
- `edge-json`
- `scheduled`
+- `ai-text`
+- `ai-chat`
+- `ai-vision`
+- `ai-stt`
+- `ai-tts`
+- `ai-image`
+- `ai-embeddings`
- `kv-api`
- `d1-api`
- `r2-api`
+AI templates:
+
+- `ai-text`: backward-compatible simple text inference scaffold, now using `@cf/moonshotai/kimi-k2.5`
+- `ai-chat`: chat-style text generation scaffold using `@cf/moonshotai/kimi-k2.5`
+- `ai-vision`: vision scaffold using `@cf/moonshotai/kimi-k2.5`
+- `ai-stt`: speech-to-text scaffold using `@cf/deepgram/nova-3`
+- `ai-tts`: text-to-speech scaffold using `@cf/deepgram/aura-2-en`
+- `ai-image`: text-to-image scaffold using `@cf/black-forest-labs/flux-2-klein-9b`
+- `ai-embeddings`: embeddings scaffold using `@cf/qwen/qwen3-embedding-0.6b`
+
+### `ai`
+
+Manage the Workers AI binding stored in local project config.
+
+```bash
+./flare-edge-cli ai binding set [--path
] [--binding ] [--remote] [--json]
+./flare-edge-cli ai binding clear [--path ] [--json]
+```
+
+Notes:
+
+- `ai binding set` updates both `flare-edge.json` and `wrangler.jsonc`
+- the default binding name is `AI`
+- `--remote` should remain enabled for Workers AI development
+
### `compat`
Run the Go/Wasm compatibility analyzer or inspect built-in rules.
@@ -427,6 +460,30 @@ Scaffold a JSON worker:
./flare-edge-cli init test-project --module-path github.com/example/test-project --template edge-json
```
+Scaffold a Go-based AI Worker:
+
+```bash
+./flare-edge-cli init ai-worker --module-path github.com/example/ai-worker --template ai-text
+```
+
+Scaffold a vision Worker:
+
+```bash
+./flare-edge-cli init ai-vision-worker --module-path github.com/example/ai-vision-worker --template ai-vision
+```
+
+Scaffold a speech-to-text Worker:
+
+```bash
+./flare-edge-cli init ai-stt-worker --module-path github.com/example/ai-stt-worker --template ai-stt
+```
+
+Add Workers AI to an existing project:
+
+```bash
+./flare-edge-cli ai binding set --path ./ai-worker --binding AI --remote
+```
+
Check compatibility and emit JSON:
```bash
diff --git a/internal/app/app.go b/internal/app/app.go
index 4b7f9de..47308e2 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -11,6 +11,7 @@ import (
"github.com/paolo/flare-edge-cli/internal/infra/toolchain"
"github.com/paolo/flare-edge-cli/internal/infra/wrangler"
"github.com/paolo/flare-edge-cli/internal/logging"
+ aisvc "github.com/paolo/flare-edge-cli/internal/service/ai"
authsvc "github.com/paolo/flare-edge-cli/internal/service/auth"
buildsvc "github.com/paolo/flare-edge-cli/internal/service/build"
compatsvc "github.com/paolo/flare-edge-cli/internal/service/compat"
@@ -43,6 +44,7 @@ func Run() int {
deps := cli.Dependencies{
Services: cli.Services{
+ AI: aisvc.NewService(store, filesystem),
Auth: authsvc.NewService(wranglerClient, authState),
Build: buildService,
Compat: compatsvc.NewService(),
diff --git a/internal/cli/ai.go b/internal/cli/ai.go
new file mode 100644
index 0000000..1dbaaea
--- /dev/null
+++ b/internal/cli/ai.go
@@ -0,0 +1,58 @@
+package cli
+
+import (
+ "context"
+
+ "github.com/spf13/cobra"
+
+ "github.com/paolo/flare-edge-cli/internal/output"
+ aisvc "github.com/paolo/flare-edge-cli/internal/service/ai"
+)
+
+func newAICommand(deps Dependencies) *cobra.Command {
+ cmd := &cobra.Command{Use: "ai", Short: "Manage Workers AI bindings"}
+ binding := &cobra.Command{Use: "binding", Short: "Manage Workers AI binding config"}
+ binding.AddCommand(newAIBindingSetCommand(deps), newAIBindingClearCommand(deps))
+ cmd.AddCommand(binding)
+ return cmd
+}
+
+func newAIBindingSetCommand(deps Dependencies) *cobra.Command {
+ var options aisvc.SetBindingOptions
+ var jsonOutput bool
+ cmd := &cobra.Command{
+ Use: "set",
+ Short: "Configure the Workers AI binding",
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ result, err := deps.Services.AI.SetBinding(context.Background(), options)
+ if err != nil {
+ return err
+ }
+ return output.NewPrinter(cmd.OutOrStdout(), cmd.ErrOrStderr(), jsonOutput).Print(result)
+ },
+ }
+ cmd.Flags().StringVar(&options.Dir, "path", ".", "Project path")
+ cmd.Flags().StringVar(&options.Binding, "binding", "AI", "Workers AI binding name")
+ cmd.Flags().BoolVar(&options.Remote, "remote", true, "Use the remote Workers AI binding")
+ cmd.Flags().BoolVar(&jsonOutput, "json", false, "Emit machine-readable JSON")
+ return cmd
+}
+
+func newAIBindingClearCommand(deps Dependencies) *cobra.Command {
+ var options aisvc.ClearBindingOptions
+ var jsonOutput bool
+ cmd := &cobra.Command{
+ Use: "clear",
+ Short: "Remove the Workers AI binding from local config",
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ result, err := deps.Services.AI.ClearBinding(context.Background(), options)
+ if err != nil {
+ return err
+ }
+ return output.NewPrinter(cmd.OutOrStdout(), cmd.ErrOrStderr(), jsonOutput).Print(result)
+ },
+ }
+ cmd.Flags().StringVar(&options.Dir, "path", ".", "Project path")
+ cmd.Flags().BoolVar(&jsonOutput, "json", false, "Emit machine-readable JSON")
+ return cmd
+}
diff --git a/internal/cli/aliases.go b/internal/cli/aliases.go
index 983effe..0e1096b 100644
--- a/internal/cli/aliases.go
+++ b/internal/cli/aliases.go
@@ -30,7 +30,7 @@ func newInitAliasCommand(deps Dependencies) *cobra.Command {
cmd.Flags().StringVar(&options.Dir, "cwd", ".", "Target parent directory")
cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Go module path")
cmd.Flags().StringVar(&options.PackageName, "package", "", "Package name")
- cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template")
+ cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings")
cmd.Flags().StringVar(&options.CompatDate, "compat-date", "", "Compatibility date")
cmd.Flags().StringVar(&options.Env, "env", "", "Wrangler environment")
cmd.Flags().BoolVar(&options.UseJSONC, "use-jsonc", false, "Generate wrangler.jsonc")
diff --git a/internal/cli/deps.go b/internal/cli/deps.go
index 7848363..40141c0 100644
--- a/internal/cli/deps.go
+++ b/internal/cli/deps.go
@@ -1,6 +1,7 @@
package cli
import (
+ aisvc "github.com/paolo/flare-edge-cli/internal/service/ai"
authsvc "github.com/paolo/flare-edge-cli/internal/service/auth"
buildsvc "github.com/paolo/flare-edge-cli/internal/service/build"
compatsvc "github.com/paolo/flare-edge-cli/internal/service/compat"
@@ -19,6 +20,7 @@ import (
)
type Services struct {
+ AI *aisvc.Service
Auth *authsvc.Service
Build *buildsvc.Service
Compat *compatsvc.Service
diff --git a/internal/cli/project.go b/internal/cli/project.go
index 14ed34f..706d56c 100644
--- a/internal/cli/project.go
+++ b/internal/cli/project.go
@@ -41,7 +41,7 @@ func newProjectInitCommand(deps Dependencies) *cobra.Command {
cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Go module path to initialize")
cmd.Flags().StringVar(&options.PackageName, "package", "", "Package name for generated Go entrypoint")
- cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api")
+ cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings")
cmd.Flags().StringVar(&options.CompatDate, "compat-date", "", "Cloudflare compatibility date")
cmd.Flags().StringVar(&options.Env, "env", "", "Default Wrangler environment")
cmd.Flags().BoolVar(&options.UseJSONC, "use-jsonc", false, "Generate wrangler.jsonc output")
diff --git a/internal/cli/root.go b/internal/cli/root.go
index d5f9dac..d85660c 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -23,6 +23,7 @@ func NewRootCommand(deps Dependencies) *cobra.Command {
}
cmd.PersistentFlags().StringVar(&accountID, "account-id", "", "Cloudflare account ID override for all commands")
+ cmd.AddCommand(newAICommand(deps))
cmd.AddCommand(newAuthCommand(deps))
cmd.AddCommand(newBuildCommand(deps))
cmd.AddCommand(newCompatCommand(deps))
diff --git a/internal/domain/config/project.go b/internal/domain/config/project.go
index eddf011..8cc70c8 100644
--- a/internal/domain/config/project.go
+++ b/internal/domain/config/project.go
@@ -32,6 +32,7 @@ type Project struct {
type ProjectBindings struct {
Vars map[string]string `json:"vars,omitempty"`
Secrets []string `json:"secrets,omitempty"`
+ AI *AIBinding `json:"ai,omitempty"`
KV []KVBinding `json:"kv,omitempty"`
D1 []D1Binding `json:"d1,omitempty"`
R2 []R2Binding `json:"r2,omitempty"`
@@ -47,6 +48,11 @@ type Environment struct {
Bindings ProjectBindings `json:"bindings"`
}
+type AIBinding struct {
+ Binding string `json:"binding"`
+ Remote bool `json:"remote,omitempty"`
+}
+
type KVBinding struct {
Binding string `json:"binding"`
ID string `json:"id,omitempty"`
@@ -74,7 +80,7 @@ func DefaultProject(name, modulePath, packageName, template, compatDate, env str
}
entry := "./cmd/worker"
- return Project{
+ project := Project{
SchemaVersion: 1,
ProjectName: name,
ModulePath: modulePath,
@@ -96,8 +102,21 @@ func DefaultProject(name, modulePath, packageName, template, compatDate, env str
WasmExecSource: "internal/generated/wasm_exec.js",
},
}
+ if UsesAIBinding(template) {
+ project.Bindings.AI = &AIBinding{Binding: "AI", Remote: true}
+ }
+ return project
}
func (p Project) ArtifactPath() string {
return fmt.Sprintf("%s/%s", p.OutDir, p.WasmFile)
}
+
+func UsesAIBinding(template string) bool {
+ switch template {
+ case "ai-text", "ai-chat", "ai-vision", "ai-stt", "ai-tts", "ai-image", "ai-embeddings":
+ return true
+ default:
+ return false
+ }
+}
diff --git a/internal/domain/config/wrangler.go b/internal/domain/config/wrangler.go
index d1a86b1..783853f 100644
--- a/internal/domain/config/wrangler.go
+++ b/internal/domain/config/wrangler.go
@@ -5,6 +5,7 @@ type WranglerConfig struct {
Main string `json:"main"`
CompatibilityDate string `json:"compatibility_date"`
Observability *WranglerObservability `json:"observability,omitempty"`
+ AI *WranglerAIBinding `json:"ai,omitempty"`
Vars map[string]string `json:"vars,omitempty"`
KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"`
D1Databases []WranglerD1Database `json:"d1_databases,omitempty"`
@@ -19,6 +20,7 @@ type WranglerObservability struct {
type WranglerEnvConfig struct {
Name string `json:"name,omitempty"`
+ AI *WranglerAIBinding `json:"ai,omitempty"`
Vars map[string]string `json:"vars,omitempty"`
KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"`
D1Databases []WranglerD1Database `json:"d1_databases,omitempty"`
@@ -26,6 +28,11 @@ type WranglerEnvConfig struct {
Routes []WranglerRoute `json:"routes,omitempty"`
}
+type WranglerAIBinding struct {
+ Binding string `json:"binding"`
+ Remote bool `json:"remote,omitempty"`
+}
+
type WranglerKVNamespace struct {
Binding string `json:"binding"`
ID string `json:"id,omitempty"`
diff --git a/internal/service/ai/service.go b/internal/service/ai/service.go
new file mode 100644
index 0000000..eb6ca04
--- /dev/null
+++ b/internal/service/ai/service.go
@@ -0,0 +1,93 @@
+package ai
+
+import (
+ "context"
+
+ "github.com/paolo/flare-edge-cli/internal/domain/config"
+ "github.com/paolo/flare-edge-cli/internal/infra/configstore"
+ "github.com/paolo/flare-edge-cli/internal/support/fs"
+)
+
+type Service struct {
+ store *configstore.Store
+ fs *fs.FileSystem
+}
+
+type SetBindingOptions struct {
+ Dir string
+ Binding string
+ Remote bool
+}
+
+type ClearBindingOptions struct {
+ Dir string
+}
+
+type BindingResult struct {
+ Binding string `json:"binding,omitempty"`
+ Remote bool `json:"remote,omitempty"`
+ Cleared bool `json:"cleared,omitempty"`
+}
+
+func NewService(store *configstore.Store, fs *fs.FileSystem) *Service {
+ return &Service{store: store, fs: fs}
+}
+
+func (s *Service) SetBinding(_ context.Context, options SetBindingOptions) (BindingResult, error) {
+ project, wranglerCfg, err := loadProjectAndWrangler(options.Dir, s.store)
+ if err != nil {
+ return BindingResult{}, err
+ }
+
+ project.Bindings.AI = &config.AIBinding{
+ Binding: options.Binding,
+ Remote: options.Remote,
+ }
+ wranglerCfg.AI = &config.WranglerAIBinding{
+ Binding: options.Binding,
+ Remote: options.Remote,
+ }
+
+ if err := s.store.SaveProject(options.Dir, project); err != nil {
+ return BindingResult{}, err
+ }
+ if err := s.store.SaveWrangler(options.Dir, project.WranglerConfig, wranglerCfg); err != nil {
+ return BindingResult{}, err
+ }
+
+ return BindingResult{
+ Binding: options.Binding,
+ Remote: options.Remote,
+ }, nil
+}
+
+func (s *Service) ClearBinding(_ context.Context, options ClearBindingOptions) (BindingResult, error) {
+ project, wranglerCfg, err := loadProjectAndWrangler(options.Dir, s.store)
+ if err != nil {
+ return BindingResult{}, err
+ }
+
+ project.Bindings.AI = nil
+ wranglerCfg.AI = nil
+
+ if err := s.store.SaveProject(options.Dir, project); err != nil {
+ return BindingResult{}, err
+ }
+ if err := s.store.SaveWrangler(options.Dir, project.WranglerConfig, wranglerCfg); err != nil {
+ return BindingResult{}, err
+ }
+
+ return BindingResult{Cleared: true}, nil
+}
+
+func loadProjectAndWrangler(dir string, store *configstore.Store) (config.Project, config.WranglerConfig, error) {
+ project, err := store.LoadProject(dir)
+ if err != nil {
+ return config.Project{}, config.WranglerConfig{}, err
+ }
+ wranglerCfg, err := store.LoadWrangler(dir, project.WranglerConfig)
+ if err != nil {
+ return config.Project{}, config.WranglerConfig{}, err
+ }
+ return project, wranglerCfg, nil
+}
diff --git a/internal/service/ai/service_test.go b/internal/service/ai/service_test.go
new file mode 100644
index 0000000..8871624
--- /dev/null
+++ b/internal/service/ai/service_test.go
@@ -0,0 +1,85 @@
+package ai
+
+import (
+ "context"
+ "path/filepath"
+ "testing"
+
+ "github.com/paolo/flare-edge-cli/internal/domain/config"
+ "github.com/paolo/flare-edge-cli/internal/infra/configstore"
+ "github.com/paolo/flare-edge-cli/internal/support/fs"
+)
+
+func TestSetAndClearBinding(t *testing.T) {
+ t.Parallel()
+
+ filesystem := fs.New()
+ store := configstore.New(filesystem)
+ service := NewService(store, filesystem)
+ dir := t.TempDir()
+
+ project := config.DefaultProject("ai-worker", "github.com/example/ai-worker", "main", "edge-http", config.DefaultCompatibilityDate, "")
+ wranglerCfg := config.WranglerConfig{
+ Name: project.WorkerName,
+ Main: filepath.ToSlash(filepath.Join(project.OutDir, project.ShimFile)),
+ CompatibilityDate: project.CompatibilityDate,
+ }
+ if err := store.SaveProject(dir, project); err != nil {
+ t.Fatalf("save project: %v", err)
+ }
+ if err := store.SaveWrangler(dir, project.WranglerConfig, wranglerCfg); err != nil {
+ t.Fatalf("save wrangler: %v", err)
+ }
+
+ result, err := service.SetBinding(context.Background(), SetBindingOptions{
+ Dir: dir,
+ Binding: "AI",
+ Remote: true,
+ })
+ if err != nil {
+ t.Fatalf("set binding: %v", err)
+ }
+ if result.Binding != "AI" || !result.Remote {
+ t.Fatalf("unexpected set result: %+v", result)
+ }
+
+ project, err = store.LoadProject(dir)
+ if err != nil {
+ t.Fatalf("load project after set: %v", err)
+ }
+ if project.Bindings.AI == nil || project.Bindings.AI.Binding != "AI" || !project.Bindings.AI.Remote {
+ t.Fatalf("project ai binding not persisted: %+v", project.Bindings.AI)
+ }
+
+ wranglerCfg, err = store.LoadWrangler(dir, project.WranglerConfig)
+ if err != nil {
+ t.Fatalf("load wrangler after set: %v", err)
+ }
+ if wranglerCfg.AI == nil || wranglerCfg.AI.Binding != "AI" || !wranglerCfg.AI.Remote {
+ t.Fatalf("wrangler ai binding not persisted: %+v", wranglerCfg.AI)
+ }
+
+ clearResult, err := service.ClearBinding(context.Background(), ClearBindingOptions{Dir: dir})
+ if err != nil {
+ t.Fatalf("clear binding: %v", err)
+ }
+ if !clearResult.Cleared {
+ t.Fatalf("expected cleared result, got %+v", clearResult)
+ }
+
+ project, err = store.LoadProject(dir)
+ if err != nil {
+ t.Fatalf("load project after clear: %v", err)
+ }
+ if project.Bindings.AI != nil {
+ t.Fatalf("expected project ai binding to be cleared")
+ }
+
+ wranglerCfg, err = store.LoadWrangler(dir, project.WranglerConfig)
+ if err != nil {
+ t.Fatalf("load wrangler after clear: %v", err)
+ }
+ if wranglerCfg.AI != nil {
+ t.Fatalf("expected wrangler ai binding to be cleared")
+ }
+}
diff --git a/internal/service/doctor/service.go b/internal/service/doctor/service.go
index 20bcee6..26b711f 100644
--- a/internal/service/doctor/service.go
+++ b/internal/service/doctor/service.go
@@ -81,6 +81,13 @@ func (s *Service) Run(ctx context.Context, options Options) (Result, error) {
} else {
checks = append(checks, Check{Name: "compatibility-date", Status: "ok", Details: project.CompatibilityDate})
}
+ if project.Bindings.AI != nil {
+ if project.Bindings.AI.Remote {
+ checks = append(checks, Check{Name: "ai-binding", Status: "ok", Details: project.Bindings.AI.Binding + " (remote)"})
+ } else {
+ checks = append(checks, Check{Name: "ai-binding", Status: "warning", Details: "Workers AI should use remote=true for development"})
+ }
+ }
if _, err := s.build.Wasm(ctx, buildsvc.WasmOptions{Path: options.Dir, NoShim: true, OutDir: filepath.Join(options.Dir, ".doctor-dist"), Clean: true}); err != nil {
checks = append(checks, Check{Name: "wasm-build", Status: "error", Details: err.Error()})
} else {
diff --git a/internal/service/project/service.go b/internal/service/project/service.go
index c43cf8e..4d70be8 100644
--- a/internal/service/project/service.go
+++ b/internal/service/project/service.go
@@ -87,6 +87,12 @@ func (s *Service) Init(_ context.Context, options InitOptions) (InitResult, erro
CompatibilityDate: project.CompatibilityDate,
Observability: &config.WranglerObservability{Enabled: true},
}
+ if project.Bindings.AI != nil {
+ wrangler.AI = &config.WranglerAIBinding{
+ Binding: project.Bindings.AI.Binding,
+ Remote: project.Bindings.AI.Remote,
+ }
+ }
files := scaffoldFiles(project)
for path, content := range files {
diff --git a/internal/service/project/service_test.go b/internal/service/project/service_test.go
new file mode 100644
index 0000000..86edd5c
--- /dev/null
+++ b/internal/service/project/service_test.go
@@ -0,0 +1,85 @@
+package project
+
+import (
+ "context"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/paolo/flare-edge-cli/internal/infra/configstore"
+ "github.com/paolo/flare-edge-cli/internal/support/fs"
+)
+
+func TestInitAITemplate(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ template string
+ model string
+ marker string
+ }{
+ {name: "text", template: "ai-text", model: `defaultAIModel = "@cf/moonshotai/kimi-k2.5"`, marker: `messages.Call("push", user)`},
+ {name: "chat", template: "ai-chat", model: `defaultAIModel = "@cf/moonshotai/kimi-k2.5"`, marker: `messages.Call("push", user)`},
+ {name: "vision", template: "ai-vision", model: `defaultAIModel = "@cf/moonshotai/kimi-k2.5"`, marker: `image_url query parameter is required`},
+ {name: "stt", template: "ai-stt", model: `defaultAIModel = "@cf/deepgram/nova-3"`, marker: `send audio bytes in the request body`},
+ {name: "tts", template: "ai-tts", model: `defaultAIModel = "@cf/deepgram/aura-2-en"`, marker: `input.Set("speaker", speaker)`},
+ {name: "image", template: "ai-image", model: `defaultAIModel = "@cf/black-forest-labs/flux-2-klein-9b"`, marker: `form.Call("append", "prompt", prompt)`},
+ {name: "embeddings", template: "ai-embeddings", model: `defaultAIModel = "@cf/qwen/qwen3-embedding-0.6b"`, marker: `input.Set("text", text)`},
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ filesystem := fs.New()
+ store := configstore.New(filesystem)
+ service := NewService(store, filesystem)
+ root := t.TempDir()
+
+ result, err := service.Init(context.Background(), InitOptions{
+ Dir: root,
+ Name: "ai-worker",
+ ModulePath: "github.com/example/ai-worker",
+ Template: tt.template,
+ WithGit: true,
+ })
+ if err != nil {
+ t.Fatalf("init project: %v", err)
+ }
+
+ project, err := store.LoadProject(result.ProjectDir)
+ if err != nil {
+ t.Fatalf("load project: %v", err)
+ }
+ if project.Bindings.AI == nil || project.Bindings.AI.Binding != "AI" || !project.Bindings.AI.Remote {
+ t.Fatalf("expected project ai binding to be scaffolded, got %+v", project.Bindings.AI)
+ }
+
+ wranglerCfg, err := store.LoadWrangler(result.ProjectDir, project.WranglerConfig)
+ if err != nil {
+ t.Fatalf("load wrangler: %v", err)
+ }
+ if wranglerCfg.AI == nil || wranglerCfg.AI.Binding != "AI" || !wranglerCfg.AI.Remote {
+ t.Fatalf("expected wrangler ai binding to be scaffolded, got %+v", wranglerCfg.AI)
+ }
+
+ workerMainPath := filepath.Join(result.ProjectDir, "cmd/worker/main.go")
+ workerMain, err := filesystem.ReadFile(workerMainPath)
+ if err != nil {
+ t.Fatalf("read worker main: %v", err)
+ }
+ content := string(workerMain)
+ if !strings.Contains(content, tt.model) {
+ t.Fatalf("expected ai worker template to include default model %s, got %s", tt.model, content)
+ }
+ if !strings.Contains(content, `env.Get("AI")`) {
+ t.Fatalf("expected ai worker template to read env.AI")
+ }
+ if !strings.Contains(content, tt.marker) {
+ t.Fatalf("expected ai worker template to include marker %s", tt.marker)
+ }
+ })
+ }
+}
diff --git a/internal/service/project/template_ai_audio.go b/internal/service/project/template_ai_audio.go
new file mode 100644
index 0000000..98ff1ad
--- /dev/null
+++ b/internal/service/project/template_ai_audio.go
@@ -0,0 +1,143 @@
+package project
+
+func aiSTTWorkerMainTemplate() string {
+ return aiWorkerTemplate("@cf/deepgram/nova-3", `func handleRequest(this js.Value, args []js.Value) any {
+ if len(args) < 2 {
+ return textResponse(500, "missing request or env")
+ }
+
+ request := args[0]
+ env := args[1]
+ ai := env.Get("AI")
+ if ai.IsUndefined() || ai.IsNull() {
+ return textResponse(500, "Workers AI binding \"AI\" is not configured")
+ }
+ if request.Get("body").IsUndefined() || request.Get("body").IsNull() {
+ return jsonTextResponse(400, "{\"message\":\"send audio bytes in the request body\"}")
+ }
+
+ url := js.Global().Get("URL").New(request.Get("url"))
+ model := searchParam(url, "model")
+ if model == "" {
+ model = defaultAIModel
+ }
+
+ contentType := request.Get("headers").Call("get", "content-type")
+ if contentType.IsUndefined() || contentType.IsNull() || contentType.String() == "" {
+ contentType = js.ValueOf("audio/mpeg")
+ }
+
+ audio := js.Global().Get("Object").New()
+ audio.Set("body", request.Get("body"))
+ audio.Set("contentType", contentType)
+
+ input := js.Global().Get("Object").New()
+ input.Set("audio", audio)
+ input.Set("detect_language", true)
+
+ options := js.Global().Get("Object").New()
+ options.Set("returnRawResponse", true)
+
+ return promise(func(resolve, reject js.Value) {
+ var onResolve js.Func
+ var onReject js.Func
+
+ onResolve = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ if len(args) > 0 {
+ resolve.Invoke(args[0])
+ return nil
+ }
+ resolve.Invoke(jsonTextResponse(502, "{\"message\":\"Workers AI returned no response\"}"))
+ return nil
+ })
+
+ onReject = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ message := "Workers AI request failed"
+ status := 502
+ if len(args) > 0 {
+ message = errorPayload(args[0])
+ if code := errorStatus(args[0]); code > 0 {
+ status = code
+ }
+ }
+ resolve.Invoke(jsonTextResponse(status, message))
+ return nil
+ })
+
+ ai.Call("run", model, input, options).Call("then", onResolve).Call("catch", onReject)
+ })
+}`)
+}
+
+func aiTTSWorkerMainTemplate() string {
+ return aiWorkerTemplate("@cf/deepgram/aura-2-en", `func handleRequest(this js.Value, args []js.Value) any {
+ if len(args) < 2 {
+ return textResponse(500, "missing request or env")
+ }
+
+ request := args[0]
+ env := args[1]
+ ai := env.Get("AI")
+ if ai.IsUndefined() || ai.IsNull() {
+ return textResponse(500, "Workers AI binding \"AI\" is not configured")
+ }
+
+ url := js.Global().Get("URL").New(request.Get("url"))
+ text := searchParam(url, "text")
+ if text == "" {
+ text = "Hello from a Go-based Cloudflare text to speech worker."
+ }
+ model := searchParam(url, "model")
+ if model == "" {
+ model = defaultAIModel
+ }
+ speaker := searchParam(url, "speaker")
+ if speaker == "" {
+ speaker = "luna"
+ }
+
+ input := js.Global().Get("Object").New()
+ input.Set("text", text)
+ input.Set("speaker", speaker)
+
+ options := js.Global().Get("Object").New()
+ options.Set("returnRawResponse", true)
+
+ return promise(func(resolve, reject js.Value) {
+ var onResolve js.Func
+ var onReject js.Func
+
+ onResolve = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ if len(args) > 0 {
+ resolve.Invoke(args[0])
+ return nil
+ }
+ resolve.Invoke(jsonTextResponse(502, "{\"message\":\"Workers AI returned no response\"}"))
+ return nil
+ })
+
+ onReject = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ message := "Workers AI request failed"
+ status := 502
+ if len(args) > 0 {
+ message = errorPayload(args[0])
+ if code := errorStatus(args[0]); code > 0 {
+ status = code
+ }
+ }
+ resolve.Invoke(jsonTextResponse(status, message))
+ return nil
+ })
+
+ ai.Call("run", model, input, options).Call("then", onResolve).Call("catch", onReject)
+ })
+}`)
+}
diff --git a/internal/service/project/template_ai_chat.go b/internal/service/project/template_ai_chat.go
new file mode 100644
index 0000000..250db81
--- /dev/null
+++ b/internal/service/project/template_ai_chat.go
@@ -0,0 +1,69 @@
+package project
+
+func aiChatWorkerMainTemplate() string {
+ return aiWorkerTemplate("@cf/moonshotai/kimi-k2.5", `func handleRequest(this js.Value, args []js.Value) any {
+ if len(args) < 2 {
+ return textResponse(500, "missing request or env")
+ }
+
+ request := args[0]
+ env := args[1]
+ ai := env.Get("AI")
+ if ai.IsUndefined() || ai.IsNull() {
+ return textResponse(500, "Workers AI binding \"AI\" is not configured")
+ }
+
+ url := js.Global().Get("URL").New(request.Get("url"))
+ prompt := searchParam(url, "prompt")
+ if prompt == "" {
+ prompt = "Say hello from a Go-based Cloudflare AI Worker."
+ }
+ model := searchParam(url, "model")
+ if model == "" {
+ model = defaultAIModel
+ }
+
+ input := js.Global().Get("Object").New()
+ messages := js.Global().Get("Array").New()
+ user := js.Global().Get("Object").New()
+ user.Set("role", "user")
+ user.Set("content", prompt)
+ messages.Call("push", user)
+ input.Set("messages", messages)
+
+ return promise(func(resolve, reject js.Value) {
+ var onResolve js.Func
+ var onReject js.Func
+
+ onResolve = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+
+ payload := "null"
+ if len(args) > 0 {
+ payload = stringify(args[0])
+ }
+ resolve.Invoke(jsonResponse(payload))
+ return nil
+ })
+
+ onReject = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+
+ message := "Workers AI request failed"
+ status := 502
+ if len(args) > 0 {
+ message = errorPayload(args[0])
+ if code := errorStatus(args[0]); code > 0 {
+ status = code
+ }
+ }
+ resolve.Invoke(jsonTextResponse(status, message))
+ return nil
+ })
+
+ ai.Call("run", model, input).Call("then", onResolve).Call("catch", onReject)
+ })
+}`)
+}
diff --git a/internal/service/project/template_ai_common.go b/internal/service/project/template_ai_common.go
new file mode 100644
index 0000000..773ebb2
--- /dev/null
+++ b/internal/service/project/template_ai_common.go
@@ -0,0 +1,125 @@
+package project
+
+import (
+ "strconv"
+ "strings"
+)
+
+func aiWorkerTemplate(defaultModel, handleRequest string) string {
+ parts := []string{
+ "package main\n\n",
+ "import \"syscall/js\"\n\n",
+ "const defaultAIModel = " + strconv.Quote(defaultModel) + "\n\n",
+ "func main() {\n",
+ "\tjs.Global().Set(\"handleRequest\", js.FuncOf(handleRequest))\n",
+ "\tjs.Global().Set(\"handleScheduled\", js.FuncOf(func(this js.Value, args []js.Value) any {\n",
+ "\t\treturn nil\n",
+ "\t}))\n",
+ "\tselect {}\n",
+ "}\n\n",
+ handleRequest,
+ "\n\n",
+ aiWorkerCommonRuntime,
+ }
+ return strings.Join(parts, "")
+}
+
+const aiWorkerCommonRuntime = `func promise(fn func(resolve js.Value, reject js.Value)) js.Value {
+ executor := js.FuncOf(func(this js.Value, args []js.Value) any {
+ if len(args) >= 2 {
+ fn(args[0], args[1])
+ }
+ return nil
+ })
+ defer executor.Release()
+
+ return js.Global().Get("Promise").New(executor)
+}
+
+func stringify(value js.Value) string {
+ output := js.Global().Get("JSON").Call("stringify", value)
+ if output.IsUndefined() || output.IsNull() {
+ return "null"
+ }
+ return output.String()
+}
+
+func errorPayload(value js.Value) string {
+ payload := js.Global().Get("Object").New()
+ payload.Set("name", value.Get("name"))
+ payload.Set("message", value.Get("message"))
+ payload.Set("code", firstDefined(value, "code", "status"))
+ payload.Set("internalCode", firstDefined(value, "internalCode", "internal_code"))
+
+ cause := value.Get("cause")
+ if !cause.IsUndefined() && !cause.IsNull() {
+ causePayload := js.Global().Get("Object").New()
+ causePayload.Set("name", cause.Get("name"))
+ causePayload.Set("message", cause.Get("message"))
+ causePayload.Set("code", firstDefined(cause, "code", "status"))
+ causePayload.Set("internalCode", firstDefined(cause, "internalCode", "internal_code"))
+ payload.Set("cause", causePayload)
+ }
+
+ text := stringify(payload)
+ if text == "{}" || text == "null" {
+ return "{\"message\":\"Workers AI request failed\"}"
+ }
+ return text
+}
+
+func errorStatus(value js.Value) int {
+ for _, key := range []string{"status", "statusCode", "code"} {
+ field := value.Get(key)
+ if !field.IsUndefined() && !field.IsNull() && field.Type() == js.TypeNumber {
+ return field.Int()
+ }
+ }
+ return 0
+}
+
+func firstDefined(value js.Value, keys ...string) js.Value {
+ for _, key := range keys {
+ field := value.Get(key)
+ if !field.IsUndefined() && !field.IsNull() {
+ return field
+ }
+ }
+ return js.Null()
+}
+
+func searchParam(url js.Value, key string) string {
+ value := url.Get("searchParams").Call("get", key)
+ if value.IsUndefined() || value.IsNull() {
+ return ""
+ }
+ return value.String()
+}
+
+func jsonResponse(body string) js.Value {
+ headers := js.Global().Get("Headers").New()
+ headers.Call("set", "content-type", "application/json")
+ options := js.Global().Get("Object").New()
+ options.Set("status", 200)
+ options.Set("headers", headers)
+ return js.Global().Get("Response").New(body, options)
+}
+
+func jsonTextResponse(status int, body string) js.Value {
+ headers := js.Global().Get("Headers").New()
+ headers.Call("set", "content-type", "application/json")
+ options := js.Global().Get("Object").New()
+ options.Set("status", status)
+ options.Set("headers", headers)
+ return js.Global().Get("Response").New(body, options)
+}
+
+func textResponse(status int, body string) js.Value {
+ headers := js.Global().Get("Headers").New()
+ headers.Call("set", "content-type", "text/plain; charset=utf-8")
+ options := js.Global().Get("Object").New()
+ options.Set("status", status)
+ options.Set("headers", headers)
+ return js.Global().Get("Response").New(body, options)
+}
+`
diff --git a/internal/service/project/template_ai_embeddings.go b/internal/service/project/template_ai_embeddings.go
new file mode 100644
index 0000000..420a1f9
--- /dev/null
+++ b/internal/service/project/template_ai_embeddings.go
@@ -0,0 +1,62 @@
+package project
+
+func aiEmbeddingsWorkerMainTemplate() string {
+ return aiWorkerTemplate("@cf/qwen/qwen3-embedding-0.6b", `func handleRequest(this js.Value, args []js.Value) any {
+ if len(args) < 2 {
+ return textResponse(500, "missing request or env")
+ }
+
+ request := args[0]
+ env := args[1]
+ ai := env.Get("AI")
+ if ai.IsUndefined() || ai.IsNull() {
+ return textResponse(500, "Workers AI binding \"AI\" is not configured")
+ }
+
+ url := js.Global().Get("URL").New(request.Get("url"))
+ text := searchParam(url, "text")
+ if text == "" {
+ text = "Cloudflare Workers AI embeddings from Go."
+ }
+ model := searchParam(url, "model")
+ if model == "" {
+ model = defaultAIModel
+ }
+
+ input := js.Global().Get("Object").New()
+ input.Set("text", text)
+
+ return promise(func(resolve, reject js.Value) {
+ var onResolve js.Func
+ var onReject js.Func
+
+ onResolve = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ payload := "null"
+ if len(args) > 0 {
+ payload = stringify(args[0])
+ }
+ resolve.Invoke(jsonResponse(payload))
+ return nil
+ })
+
+ onReject = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ message := "Workers AI request failed"
+ status := 502
+ if len(args) > 0 {
+ message = errorPayload(args[0])
+ if code := errorStatus(args[0]); code > 0 {
+ status = code
+ }
+ }
+ resolve.Invoke(jsonTextResponse(status, message))
+ return nil
+ })
+
+ ai.Call("run", model, input).Call("then", onResolve).Call("catch", onReject)
+ })
+}`)
+}
diff --git a/internal/service/project/template_ai_image.go b/internal/service/project/template_ai_image.go
new file mode 100644
index 0000000..95031af
--- /dev/null
+++ b/internal/service/project/template_ai_image.go
@@ -0,0 +1,72 @@
+package project
+
+func aiImageWorkerMainTemplate() string {
+ return aiWorkerTemplate("@cf/black-forest-labs/flux-2-klein-9b", `func handleRequest(this js.Value, args []js.Value) any {
+ if len(args) < 2 {
+ return textResponse(500, "missing request or env")
+ }
+
+ request := args[0]
+ env := args[1]
+ ai := env.Get("AI")
+ if ai.IsUndefined() || ai.IsNull() {
+ return textResponse(500, "Workers AI binding \"AI\" is not configured")
+ }
+
+ url := js.Global().Get("URL").New(request.Get("url"))
+ prompt := searchParam(url, "prompt")
+ if prompt == "" {
+ prompt = "A dramatic cloudscape over the Alps at sunset."
+ }
+ model := searchParam(url, "model")
+ if model == "" {
+ model = defaultAIModel
+ }
+
+ form := js.Global().Get("FormData").New()
+ form.Call("append", "prompt", prompt)
+ form.Call("append", "width", "1024")
+ form.Call("append", "height", "1024")
+
+ formResponse := js.Global().Get("Response").New(form)
+ multipart := js.Global().Get("Object").New()
+ multipart.Set("body", formResponse.Get("body"))
+ multipart.Set("contentType", formResponse.Get("headers").Call("get", "content-type"))
+
+ input := js.Global().Get("Object").New()
+ input.Set("multipart", multipart)
+
+ return promise(func(resolve, reject js.Value) {
+ var onResolve js.Func
+ var onReject js.Func
+
+ onResolve = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ payload := "null"
+ if len(args) > 0 {
+ payload = stringify(args[0])
+ }
+ resolve.Invoke(jsonResponse(payload))
+ return nil
+ })
+
+ onReject = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+ message := "Workers AI request failed"
+ status := 502
+ if len(args) > 0 {
+ message = errorPayload(args[0])
+ if code := errorStatus(args[0]); code > 0 {
+ status = code
+ }
+ }
+ resolve.Invoke(jsonTextResponse(status, message))
+ return nil
+ })
+
+ ai.Call("run", model, input).Call("then", onResolve).Call("catch", onReject)
+ })
+}`)
+}
diff --git a/internal/service/project/template_ai_vision.go b/internal/service/project/template_ai_vision.go
new file mode 100644
index 0000000..7e337d3
--- /dev/null
+++ b/internal/service/project/template_ai_vision.go
@@ -0,0 +1,88 @@
+package project
+
+func aiVisionWorkerMainTemplate() string {
+ return aiWorkerTemplate("@cf/moonshotai/kimi-k2.5", `func handleRequest(this js.Value, args []js.Value) any {
+ if len(args) < 2 {
+ return textResponse(500, "missing request or env")
+ }
+
+ request := args[0]
+ env := args[1]
+ ai := env.Get("AI")
+ if ai.IsUndefined() || ai.IsNull() {
+ return textResponse(500, "Workers AI binding \"AI\" is not configured")
+ }
+
+ url := js.Global().Get("URL").New(request.Get("url"))
+ prompt := searchParam(url, "prompt")
+ if prompt == "" {
+ prompt = "Describe this image."
+ }
+ imageURL := searchParam(url, "image_url")
+ if imageURL == "" {
+ return jsonTextResponse(400, "{\"message\":\"image_url query parameter is required\"}")
+ }
+ model := searchParam(url, "model")
+ if model == "" {
+ model = defaultAIModel
+ }
+
+ content := js.Global().Get("Array").New()
+
+ textPart := js.Global().Get("Object").New()
+ textPart.Set("type", "text")
+ textPart.Set("text", prompt)
+ content.Call("push", textPart)
+
+ imagePart := js.Global().Get("Object").New()
+ imagePart.Set("type", "image_url")
+ imageURLPayload := js.Global().Get("Object").New()
+ imageURLPayload.Set("url", imageURL)
+ imagePart.Set("image_url", imageURLPayload)
+ content.Call("push", imagePart)
+
+ messages := js.Global().Get("Array").New()
+ user := js.Global().Get("Object").New()
+ user.Set("role", "user")
+ user.Set("content", content)
+ messages.Call("push", user)
+
+ input := js.Global().Get("Object").New()
+ input.Set("messages", messages)
+
+ return promise(func(resolve, reject js.Value) {
+ var onResolve js.Func
+ var onReject js.Func
+
+ onResolve = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+
+ payload := "null"
+ if len(args) > 0 {
+ payload = stringify(args[0])
+ }
+ resolve.Invoke(jsonResponse(payload))
+ return nil
+ })
+
+ onReject = js.FuncOf(func(this js.Value, args []js.Value) any {
+ defer onResolve.Release()
+ defer onReject.Release()
+
+ message := "Workers AI request failed"
+ status := 502
+ if len(args) > 0 {
+ message = errorPayload(args[0])
+ if code := errorStatus(args[0]); code > 0 {
+ status = code
+ }
+ }
+ resolve.Invoke(jsonTextResponse(status, message))
+ return nil
+ })
+
+ ai.Call("run", model, input).Call("then", onResolve).Call("catch", onReject)
+ })
+}`)
+}
diff --git a/internal/service/project/templates.go b/internal/service/project/templates.go
index 1bdae33..dba39c0 100644
--- a/internal/service/project/templates.go
+++ b/internal/service/project/templates.go
@@ -23,20 +23,35 @@ func readmeTemplate(project config.Project) string {
}
func workerMainTemplate(project config.Project) string {
- message := "hello from flare-edge-cli"
switch project.Template {
+ case "ai-text", "ai-chat":
+ return aiChatWorkerMainTemplate()
+ case "ai-vision":
+ return aiVisionWorkerMainTemplate()
+ case "ai-stt":
+ return aiSTTWorkerMainTemplate()
+ case "ai-tts":
+ return aiTTSWorkerMainTemplate()
+ case "ai-image":
+ return aiImageWorkerMainTemplate()
+ case "ai-embeddings":
+ return aiEmbeddingsWorkerMainTemplate()
case "edge-json":
- message = "{\"ok\":true}"
+ return basicWorkerMainTemplate("{\"ok\":true}")
case "scheduled":
- message = "scheduled handler ready"
+ return basicWorkerMainTemplate("scheduled handler ready")
case "kv-api":
- message = "kv binding ready"
+ return basicWorkerMainTemplate("kv binding ready")
case "d1-api":
- message = "d1 binding ready"
+ return basicWorkerMainTemplate("d1 binding ready")
case "r2-api":
- message = "r2 binding ready"
+ return basicWorkerMainTemplate("r2 binding ready")
+ default:
+ return basicWorkerMainTemplate("hello from flare-edge-cli")
}
+}
+func basicWorkerMainTemplate(message string) string {
return fmt.Sprintf(`package main
import "syscall/js"
diff --git a/internal/service/teardown/service.go b/internal/service/teardown/service.go
index 390fb5c..2af0c62 100644
--- a/internal/service/teardown/service.go
+++ b/internal/service/teardown/service.go
@@ -247,9 +247,11 @@ func resetLocalState(project config.Project, wranglerCfg config.WranglerConfig,
if keepBindings {
return project, wranglerCfg
}
+ project.Bindings.AI = nil
project.Bindings.KV = nil
project.Bindings.D1 = nil
project.Bindings.R2 = nil
+ wranglerCfg.AI = nil
wranglerCfg.KVNamespaces = nil
wranglerCfg.D1Databases = nil
wranglerCfg.R2Buckets = nil
diff --git a/internal/service/teardown/service_test.go b/internal/service/teardown/service_test.go
index 7bae275..a79ec54 100644
--- a/internal/service/teardown/service_test.go
+++ b/internal/service/teardown/service_test.go
@@ -14,12 +14,14 @@ func TestResetLocalState(t *testing.T) {
Bindings: config.ProjectBindings{
Vars: map[string]string{"KEEP": "1"},
Secrets: []string{"SECRET"},
+ AI: &config.AIBinding{Binding: "AI", Remote: true},
KV: []config.KVBinding{{Binding: "KV", ID: "kv-id"}},
D1: []config.D1Binding{{Binding: "DB", DatabaseName: "db"}},
R2: []config.R2Binding{{Binding: "BUCKET", BucketName: "bucket"}},
},
}
wrangler := config.WranglerConfig{
+ AI: &config.WranglerAIBinding{Binding: "AI", Remote: true},
Routes: []config.WranglerRoute{{Pattern: "example.com/*"}},
KVNamespaces: []config.WranglerKVNamespace{{Binding: "KV", ID: "kv-id"}},
D1Databases: []config.WranglerD1Database{{Binding: "DB", DatabaseName: "db"}},
@@ -33,10 +35,13 @@ func TestResetLocalState(t *testing.T) {
if len(cleanProject.Bindings.Secrets) != 0 {
t.Fatalf("expected secrets to be cleared")
}
+ if cleanProject.Bindings.AI != nil {
+ t.Fatalf("expected ai binding to be cleared")
+ }
if got := cleanProject.Bindings.Vars["KEEP"]; got != "1" {
t.Fatalf("expected vars to be preserved, got %q", got)
}
- if len(cleanWrangler.Routes) != 0 || len(cleanWrangler.KVNamespaces) != 0 || len(cleanWrangler.D1Databases) != 0 || len(cleanWrangler.R2Buckets) != 0 {
+ if cleanWrangler.AI != nil || len(cleanWrangler.Routes) != 0 || len(cleanWrangler.KVNamespaces) != 0 || len(cleanWrangler.D1Databases) != 0 || len(cleanWrangler.R2Buckets) != 0 {
t.Fatalf("expected wrangler resources to be cleared")
}
@@ -47,9 +52,15 @@ func TestResetLocalState(t *testing.T) {
if len(keptProject.Bindings.Secrets) != 0 {
t.Fatalf("expected secrets to be cleared even when keeping bindings")
}
+ if keptProject.Bindings.AI == nil {
+ t.Fatalf("expected ai binding to be preserved when keep-bindings is true")
+ }
if len(keptWrangler.Routes) != 0 {
t.Fatalf("expected routes to be cleared")
}
+ if keptWrangler.AI == nil {
+ t.Fatalf("expected ai binding to be preserved when keep-bindings is true")
+ }
if len(keptWrangler.KVNamespaces) != 1 || len(keptWrangler.D1Databases) != 1 || len(keptWrangler.R2Buckets) != 1 {
t.Fatalf("expected wrangler bindings to be preserved when keep-bindings is true")
}