diff --git a/go/plugins/anthropic/anthropic.go b/go/plugins/anthropic/anthropic.go
new file mode 100644
index 000000000..60e5fe5a3
--- /dev/null
+++ b/go/plugins/anthropic/anthropic.go
@@ -0,0 +1,145 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package anthropic
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"sync"
+
+	"github.com/anthropics/anthropic-sdk-go"
+	"github.com/anthropics/anthropic-sdk-go/option"
+	"github.com/firebase/genkit/go/ai"
+	"github.com/firebase/genkit/go/core"
+	"github.com/firebase/genkit/go/genkit"
+	ant "github.com/firebase/genkit/go/plugins/internal/anthropic"
+)
+
+const (
+	provider             = "anthropic"
+	anthropicLabelPrefix = "Anthropic"
+)
+
+type Anthropic struct {
+	APIKey  string           // If not provided, defaults to ANTHROPIC_API_KEY
+	aclient anthropic.Client // Anthropic client
+	mu      sync.Mutex       // Mutex to control access
+	initted bool             // Whether the plugin has been initialized
+}
+
+func (a *Anthropic) Name() string {
+	return provider
+}
+
+func (a *Anthropic) Init(ctx context.Context, g *genkit.Genkit) (err error) {
+	if a == nil {
+		a = &Anthropic{}
+	}
+
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	if a.initted {
+		return errors.New("plugin already initialized")
+	}
+	defer func() {
+		if err != nil {
+			err = fmt.Errorf("Anthropic.Init: %w", err)
+		}
+	}()
+
+	apiKey := a.APIKey
+	if apiKey == "" {
+		apiKey = os.Getenv("ANTHROPIC_API_KEY")
+	}
+	if apiKey == "" {
+		return fmt.Errorf("Anthropic requires setting ANTHROPIC_API_KEY in the environment")
+	}
+
+	ac := anthropic.NewClient(
+		option.WithAPIKey(apiKey),
+	)
+	a.aclient = ac
+	a.initted = true
+
+	return nil
+}
+
+func (a *Anthropic) DefineModel(g *genkit.Genkit, name string, info *ai.ModelInfo) ai.Model {
+	return ant.DefineModel(g, a.aclient, provider, name, *info)
+}
+
+func (a *Anthropic) IsDefinedModel(g *genkit.Genkit, name string) bool {
+	return genkit.LookupModel(g, provider, name) != nil
+}
+
+func (a *Anthropic) ListActions(ctx context.Context) []core.ActionDesc {
+	actions := []core.ActionDesc{}
+
+	models, err := listModels(ctx, &a.aclient)
+	if err != nil {
+		return nil
+	}
+
+	for _, name := range models {
+		metadata := map[string]any{
+			"model": map[string]any{
+				"supports": map[string]any{
+					"media":       true,
+					"multiturn":   true,
+					"systemRole":  true,
+					"tools":       true,
+					"toolChoice":  true,
+					"constrained": "no-tools",
+				},
+			},
+			"versions": []string{},
+			"stage":    string(ai.ModelStageStable),
+		}
+		metadata["label"] = fmt.Sprintf("%s - %s", anthropicLabelPrefix, name)
+
+		actions = append(actions, core.ActionDesc{
+			Type:     core.ActionTypeModel,
+			Name:     fmt.Sprintf("%s/%s", provider, name),
+			Key:      fmt.Sprintf("/%s/%s/%s", core.ActionTypeModel, provider, name),
+			Metadata: metadata,
+		})
+
+	}
+	return actions
+}
+
+func (a *Anthropic) ResolveAction(g *genkit.Genkit, atype core.ActionType, name string) error {
+	switch atype {
+	case core.ActionTypeModel:
+		ant.DefineModel(g, a.aclient, provider, name, ai.ModelInfo{
+			Label:    fmt.Sprintf("%s - %s", anthropicLabelPrefix, name),
+			Stage:    ai.ModelStageStable,
+			Versions: []string{},
+			Supports: &ai.ModelSupports{
+				Media:       true,
+				Multiturn:   true,
+				SystemRole:  true,
+				Tools:       true,
+				ToolChoice:  true,
+				Constrained: ai.ConstrainedSupportNoTools,
+			},
+		})
+	}
+	return nil
+}
diff --git a/go/plugins/anthropic/models.go b/go/plugins/anthropic/models.go
new file mode 100644
index 000000000..2f00ababb
--- /dev/null
+++ b/go/plugins/anthropic/models.go
@@ -0,0 +1,40 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package anthropic
+
+import (
+	"context"
+
+	"github.com/anthropics/anthropic-sdk-go"
+)
+
+// listModels returns a list of model names supported by the Anthropic client
+func listModels(ctx context.Context, client *anthropic.Client) ([]string, error) {
+	iter := client.Models.ListAutoPaging(ctx, anthropic.ModelListParams{})
+	models := []string{}
+
+	for iter.Next() {
+		m := iter.Current()
+		models = append(models, m.DisplayName)
+	}
+
+	if err := iter.Err(); err != nil {
+		panic(err.Error())
+	}
+
+	return models, nil
+}
diff --git a/go/plugins/googlegenai/googlegenai.go b/go/plugins/googlegenai/googlegenai.go
index f5c29183c..5ab3c25f8 100644
--- a/go/plugins/googlegenai/googlegenai.go
+++ b/go/plugins/googlegenai/googlegenai.go
@@ -330,7 +330,7 @@ func (ga *GoogleAI) ListActions(ctx context.Context) []core.ActionDesc {
 					"systemRole":  true,
 					"tools":       true,
 					"toolChoice":  true,
-					"constrained": true,
+					"constrained": "no-tools",
 				},
 				"versions": []string{},
 				"stage":    string(ai.ModelStageStable),
@@ -394,7 +394,7 @@ func (v *VertexAI) ListActions(ctx context.Context) []core.ActionDesc {
 					"systemRole":  true,
 					"tools":       true,
 					"toolChoice":  true,
-					"constrained": true,
+					"constrained": "no-tools",
 				},
 				"versions": []string{},
 				"stage":    string(ai.ModelStageStable),
diff --git a/go/plugins/internal/anthropic/anthropic.go b/go/plugins/internal/anthropic/anthropic.go
new file mode 100644
index 000000000..c77a43624
--- /dev/null
+++ b/go/plugins/internal/anthropic/anthropic.go
@@ -0,0 +1,339 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package anthropic
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+
+	"github.com/firebase/genkit/go/ai"
+	"github.com/firebase/genkit/go/genkit"
+	"github.com/firebase/genkit/go/plugins/internal/uri"
+	"github.com/invopop/jsonschema"
+
+	"github.com/anthropics/anthropic-sdk-go"
+)
+
+const (
+	ToolNameRegex = `^[a-zA-Z0-9_-]{1,64}$`
+)
+
+func DefineModel(g *genkit.Genkit, client anthropic.Client, provider, name string, info ai.ModelInfo) ai.Model {
+	label := "Anthropic"
+	if provider == "vertexai" {
+		label = "Vertex AI"
+	}
+	meta := &ai.ModelInfo{
+		Label:    label + "-" + name,
+		Supports: info.Supports,
+		Versions: info.Versions,
+	}
+	return genkit.DefineModel(g, provider, name, meta, func(
+		ctx context.Context,
+		input *ai.ModelRequest,
+		cb func(context.Context, *ai.ModelResponseChunk) error,
+	) (*ai.ModelResponse, error) {
+		return generate(ctx, client, name, input, cb)
+	})
+}
+
+// Generate function defines how a generate request is done in Anthropic models
+func generate(
+	ctx context.Context,
+	client anthropic.Client,
+	model string,
+	input *ai.ModelRequest,
+	cb func(context.Context, *ai.ModelResponseChunk) error,
+) (*ai.ModelResponse, error) {
+	req, err := toAnthropicRequest(model, input)
+	if err != nil {
+		return nil, fmt.Errorf("unable to generate anthropic request: %w", err)
+	}
+
+	// no streaming
+	if cb == nil {
+		msg, err := client.Messages.New(ctx, *req)
+		if err != nil {
+			return nil, err
+		}
+
+		r, err := toGenkitResponse(msg)
+		if err != nil {
+			return nil, err
+		}
+
+		r.Request = input
+		return r, nil
+	} else {
+		stream := client.Messages.NewStreaming(ctx, *req)
+		message := anthropic.Message{}
+		for stream.Next() {
+			event := stream.Current()
+			err := message.Accumulate(event)
+			if err != nil {
+				return nil, err
+			}
+
+			content := []*ai.Part{}
+			switch event := event.AsAny().(type) {
+			case anthropic.ContentBlockDeltaEvent:
+				content = append(content, ai.NewTextPart(event.Delta.Text))
+				cb(ctx, &ai.ModelResponseChunk{
+					Content: content,
+				})
+			case anthropic.MessageStopEvent:
+				r, err := toGenkitResponse(&message)
+				if err != nil {
+					return nil, err
+				}
+				r.Request = input
+				return r, nil
+			}
+		}
+		if stream.Err() != nil {
+			return nil, stream.Err()
+		}
+	}
+
+	return nil, nil
+}
+
+func toAnthropicRole(role ai.Role) (anthropic.MessageParamRole, error) {
+	switch role {
+	case ai.RoleUser:
+		return anthropic.MessageParamRoleUser, nil
+	case ai.RoleModel:
+		return anthropic.MessageParamRoleAssistant, nil
+	case ai.RoleTool:
+		return anthropic.MessageParamRoleAssistant, nil
+	default:
+		return "", fmt.Errorf("unknown role given: %q", role)
+	}
+}
+
+// toAnthropicRequest translates [ai.ModelRequest] to an Anthropic request
+func toAnthropicRequest(model string, i *ai.ModelRequest) (*anthropic.MessageNewParams, error) {
+	messages := make([]anthropic.MessageParam, 0)
+
+	req, err := configFromRequest(i)
+	if err != nil {
+		return nil, err
+	}
+
+	if req.Model == "" {
+		return nil, errors.New("anthropic model not provided in request")
+	}
+	// configure system prompt (if given)
+	sysBlocks := []anthropic.TextBlockParam{}
+	for _, message := range i.Messages {
+		if message.Role == ai.RoleSystem {
+			// only text is supported for system messages
+			sysBlocks = append(sysBlocks, anthropic.TextBlockParam{Text: message.Text()})
+		} else if message.Content[len(message.Content)-1].IsToolResponse() {
+			// if the last message is a ToolResponse, the conversation must continue
+			// and the ToolResponse message must be sent as a user
+			// see: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#handling-tool-use-and-tool-result-content-blocks
+			parts, err := toAnthropicParts(message.Content)
+			if err != nil {
+				return nil, err
+			}
+			messages = append(messages, anthropic.NewUserMessage(parts...))
+		} else {
+			parts, err := toAnthropicParts(message.Content)
+			if err != nil {
+				return nil, err
+			}
+			role, err := toAnthropicRole(message.Role)
+			if err != nil {
+				return nil, err
+			}
+			messages = append(messages, anthropic.MessageParam{
+				Role:    role,
+				Content: parts,
+			})
+		}
+	}
+
+	req.System = sysBlocks
+	req.Messages = messages
+
+	tools, err := toAnthropicTools(i.Tools)
+	if err != nil {
+		return nil, err
+	}
+	req.Tools = tools
+
+	return req, nil
+}
+
+// mapToStruct unmarshals a map[String]any to the expected type
+func mapToStruct(m map[string]any, v any) error {
+	jsonData, err := json.Marshal(m)
+	if err != nil {
+		return err
+	}
+	return json.Unmarshal(jsonData, v)
+}
+
+// configFromRequest converts any supported config type to [anthropic.MessageNewParams]
+func configFromRequest(input *ai.ModelRequest) (*anthropic.MessageNewParams, error) {
+	var result anthropic.MessageNewParams
+
+	switch config := input.Config.(type) {
+	case anthropic.MessageNewParams:
+		result = config
+	case *anthropic.MessageNewParams:
+		result = *config
+	case map[string]any:
+		if err := mapToStruct(config, &result); err != nil {
+			return nil, err
+		}
+	case nil:
+		// Empty configuration is considered valid
+	default:
+		return nil, fmt.Errorf("unexpected config type: %T", input.Config)
+	}
+	return &result, nil
+}
+
+// toAnthropicTools translates [ai.ToolDefinition] to an anthropic.ToolParam type
+func toAnthropicTools(tools []*ai.ToolDefinition) ([]anthropic.ToolUnionParam, error) {
+	resp := make([]anthropic.ToolUnionParam, 0)
+	regex := regexp.MustCompile(ToolNameRegex)
+
+	for _, t := range tools {
+		if t.Name == "" {
+			return nil, fmt.Errorf("tool name is required")
+		}
+		if !regex.MatchString(t.Name) {
+			return nil, fmt.Errorf("tool name must match regex: %s", ToolNameRegex)
+		}
+
+		resp = append(resp, anthropic.ToolUnionParam{
+			OfTool: &anthropic.ToolParam{
+				Name:        t.Name,
+				Description: anthropic.String(t.Description),
+				InputSchema: toAnthropicSchema[map[string]any](),
+			},
+		})
+	}
+
+	return resp, nil
+}
+
+// toAnthropicSchema generates a JSON schema for the requested input type
+func toAnthropicSchema[T any]() anthropic.ToolInputSchemaParam {
+	reflector := jsonschema.Reflector{
+		AllowAdditionalProperties: true,
+		DoNotReference:            true,
+	}
+	var v T
+	schema := reflector.Reflect(v)
+	return anthropic.ToolInputSchemaParam{
+		Properties: schema.Properties,
+	}
+}
+
+// toAnthropicParts translates [ai.Part] to an anthropic.ContentBlockParamUnion type
+func toAnthropicParts(parts []*ai.Part) ([]anthropic.ContentBlockParamUnion, error) {
+	blocks := []anthropic.ContentBlockParamUnion{}
+
+	for _, p := range parts {
+		switch {
+		case p.IsText():
+			blocks = append(blocks, anthropic.NewTextBlock(p.Text))
+		case p.IsMedia():
+			contentType, data, _ := uri.Data(p)
+			blocks = append(blocks, anthropic.NewImageBlockBase64(contentType, base64.StdEncoding.EncodeToString(data)))
+		case p.IsData():
+			contentType, data, _ := uri.Data(p)
+			blocks = append(blocks, anthropic.NewImageBlockBase64(contentType, base64.RawStdEncoding.EncodeToString(data)))
+		case p.IsToolRequest():
+			toolReq := p.ToolRequest
+			blocks = append(blocks, anthropic.NewToolUseBlock(toolReq.Ref, toolReq.Input, toolReq.Name))
+		case p.IsToolResponse():
+			toolResp := p.ToolResponse
+			output, err := json.Marshal(toolResp.Output)
+			if err != nil {
+				return nil, fmt.Errorf("unable to parse tool response, err: %w", err)
+			}
+			blocks = append(blocks, anthropic.NewToolResultBlock(toolResp.Ref, string(output), false))
+		case p.IsReasoning():
+			signature := []byte{}
+			if p.Metadata != nil {
+				if sig, ok := p.Metadata["signature"].([]byte); ok {
+					signature = sig
+				}
+			}
+			blocks = append(blocks, anthropic.NewThinkingBlock(string(signature), p.Text))
+		default:
+			return nil, errors.New("unknown part type in the request")
+		}
+	}
+
+	return blocks, nil
+}
+
+// toGenkitResponse translates an Anthropic Message to [ai.ModelResponse]
+func toGenkitResponse(m *anthropic.Message) (*ai.ModelResponse, error) {
+	r := ai.ModelResponse{}
+
+	switch m.StopReason {
+	case anthropic.StopReasonMaxTokens:
+		r.FinishReason = ai.FinishReasonLength
+	case anthropic.StopReasonStopSequence:
+		r.FinishReason = ai.FinishReasonStop
+	case anthropic.StopReasonEndTurn:
+		r.FinishReason = ai.FinishReasonStop
+	case anthropic.StopReasonToolUse:
+		r.FinishReason = ai.FinishReasonStop
+	default:
+		r.FinishReason = ai.FinishReasonUnknown
+	}
+
+	msg := &ai.Message{}
+	msg.Role = ai.RoleModel
+	for _, part := range m.Content {
+		var p *ai.Part
+		switch part.AsAny().(type) {
+		case anthropic.ThinkingBlock:
+			p = ai.NewReasoningPart(part.Text, []byte(part.Signature))
+		case anthropic.TextBlock:
+			p = ai.NewTextPart(string(part.Text))
+		case anthropic.ToolUseBlock:
+			p = ai.NewToolRequestPart(&ai.ToolRequest{
+				Ref:   part.ID,
+				Input: part.Input,
+				Name:  part.Name,
+			})
+		default:
+			return nil, fmt.Errorf("unknown part: %#v", part)
+		}
+		msg.Content = append(msg.Content, p)
+	}
+
+	r.Message = msg
+	r.Usage = &ai.GenerationUsage{
+		InputTokens:  int(m.Usage.InputTokens),
+		OutputTokens: int(m.Usage.OutputTokens),
+	}
+	return &r, nil
+}
diff --git a/go/plugins/internal/anthropic/anthropic_test.go b/go/plugins/internal/anthropic/anthropic_test.go
new file mode 100644
index 000000000..368b90f46
--- /dev/null
+++ b/go/plugins/internal/anthropic/anthropic_test.go
@@ -0,0 +1,153 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package anthropic
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/anthropics/anthropic-sdk-go"
+	"github.com/firebase/genkit/go/ai"
+)
+
+func TestAnthropic(t *testing.T) {
+	t.Run("to anthropic role", func(t *testing.T) {
+		r, err := toAnthropicRole(ai.RoleModel)
+		if err != nil {
+			t.Error(err)
+		}
+		if r != anthropic.MessageParamRoleAssistant {
+			t.Errorf("want: %q, got: %q", anthropic.MessageParamRoleAssistant, r)
+		}
+		r, err = toAnthropicRole(ai.RoleUser)
+		if err != nil {
+			t.Error(err)
+		}
+		if r != anthropic.MessageParamRoleUser {
+			t.Errorf("want: %q, got: %q", anthropic.MessageParamRoleUser, r)
+		}
+		r, err = toAnthropicRole(ai.RoleSystem)
+		if err == nil {
+			t.Errorf("should have failed, got: %q", r)
+		}
+		r, err = toAnthropicRole(ai.RoleTool)
+		if err != nil {
+			t.Error(err)
+		}
+		if r != anthropic.MessageParamRoleAssistant {
+			t.Errorf("want: %q, got: %q", anthropic.MessageParamRoleAssistant, r)
+		}
+		r, err = toAnthropicRole("unknown")
+		if err == nil {
+			t.Errorf("should have failed, got: %q", r)
+		}
+	})
+}
+
+func TestAnthropicConfig(t *testing.T) {
+	emptyConfig := anthropic.MessageNewParams{}
+	expectedConfig := anthropic.MessageNewParams{
+		Temperature: anthropic.Float(1.0),
+		TopK:        anthropic.Int(1),
+	}
+
+	tests := []struct {
+		name           string
+		inputReq       *ai.ModelRequest
+		expectedConfig *anthropic.MessageNewParams
+		expectedErr    string
+	}{
+		{
+			name: "Input is anthropic.MessageNewParams struct",
+			inputReq: &ai.ModelRequest{
+				Config: anthropic.MessageNewParams{
+					Temperature: anthropic.Float(1.0),
+					TopK:        anthropic.Int(1),
+				},
+			},
+			expectedConfig: &expectedConfig,
+			expectedErr:    "",
+		},
+		{
+			name: "Input is *anthropic.MessageNewParams struct",
+			inputReq: &ai.ModelRequest{
+				Config: &anthropic.MessageNewParams{
+					Temperature: anthropic.Float(1.0),
+					TopK:        anthropic.Int(1),
+				},
+			},
+			expectedConfig: &expectedConfig,
+			expectedErr:    "",
+		},
+		{
+			name: "Input is map[string]any",
+			inputReq: &ai.ModelRequest{
+				Config: map[string]any{
+					"temperature": 1.0,
+					"top_k":       1,
+				},
+			},
+			expectedConfig: &expectedConfig,
+			expectedErr:    "",
+		},
+		{
+			name: "Input is map[string]any (empty)",
+			inputReq: &ai.ModelRequest{
+				Config: map[string]any{},
+			},
+			expectedConfig: &emptyConfig,
+			expectedErr:    "",
+		},
+		{
+			name: "Input is nil",
+			inputReq: &ai.ModelRequest{
+				Config: nil,
+			},
+			expectedConfig: &emptyConfig,
+			expectedErr:    "",
+		},
+		{
+			name: "Input is an unexpected type",
+			inputReq: &ai.ModelRequest{
+				Config: 123,
+			},
+			expectedConfig: &emptyConfig,
+			expectedErr:    "unexpected config type: int",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotConfig, err := configFromRequest(tt.inputReq)
+			if tt.expectedErr != "" {
+				if err == nil {
+					t.Errorf("expecting error containing %q, got nil", tt.expectedErr)
+				} else if !strings.Contains(err.Error(), tt.expectedErr) {
+					t.Errorf("expecting error to contain %q, but got: %q", tt.expectedErr, err.Error())
+				}
+				return
+			}
+			if err != nil {
+				t.Errorf("expected no error, got: %v", err)
+			}
+			if !reflect.DeepEqual(gotConfig, tt.expectedConfig) {
+				t.Errorf("configFromRequest() got config = %+v, want %+v", gotConfig, tt.expectedConfig)
+			}
+		})
+	}
+}
diff --git a/go/plugins/vertexai/modelgarden/models.go b/go/plugins/internal/anthropic/models.go
similarity index 73%
rename from go/plugins/vertexai/modelgarden/models.go
rename to go/plugins/internal/anthropic/models.go
index 7c136fc30..cbab42096 100644
--- a/go/plugins/vertexai/modelgarden/models.go
+++ b/go/plugins/internal/anthropic/models.go
@@ -14,54 +14,51 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-package modelgarden
+package anthropic
 
 import (
 	"github.com/firebase/genkit/go/ai"
 	"github.com/firebase/genkit/go/plugins/internal"
 )
 
-const provider = "vertexai"
-
-// supported anthropic models
-var anthropicModels = map[string]ai.ModelInfo{
+var AnthropicModels = map[string]ai.ModelInfo{
 	"claude-3-5-sonnet-v2": {
-		Label:    "Vertex AI Model Garden - Claude 3.5 Sonnet",
+		Label:    "Claude 3.5 Sonnet",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-3-5-sonnet-v2@20241022"},
 	},
 	"claude-3-5-sonnet": {
-		Label:    "Vertex AI Model Garden - Claude 3.5 Sonnet",
+		Label:    "Claude 3.5 Sonnet",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-3-5-sonnet@20240620"},
 	},
 	"claude-3-sonnet": {
-		Label:    "Vertex AI Model Garden - Claude 3 Sonnet",
+		Label:    "Claude 3 Sonnet",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-3-sonnet@20240229"},
 	},
 	"claude-3-haiku": {
-		Label:    "Vertex AI Model Garden - Claude 3 Haiku",
+		Label:    "Claude 3 Haiku",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-3-haiku@20240307"},
 	},
 	"claude-3-opus": {
-		Label:    "Vertex AI Model Garden - Claude 3 Opus",
+		Label:    "Claude 3 Opus",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-3-opus@20240229"},
 	},
 	"claude-3-7-sonnet": {
-		Label:    "Vertex AI Model Garden - Claude 3.7 Sonnet",
+		Label:    "Claude 3.7 Sonnet",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-3-7-sonnet@20250219"},
 	},
 	"claude-opus-4": {
-		Label:    "Vertex AI Model Garden - Claude Opus 4",
+		Label:    "Claude Opus 4",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-opus-4@20250514"},
 	},
 	"claude-sonnet-4": {
-		Label:    "Vertex AI Model Garden - Claude Sonnet 4",
+		Label:    "Claude Sonnet 4",
 		Supports: &internal.Multimodal,
 		Versions: []string{"claude-sonnet-4@20250514"},
 	},
diff --git a/go/plugins/vertexai/modelgarden/anthropic.go b/go/plugins/vertexai/modelgarden/anthropic.go
index 79a0a8003..be5cffa39 100644
--- a/go/plugins/vertexai/modelgarden/anthropic.go
+++ b/go/plugins/vertexai/modelgarden/anthropic.go
@@ -18,26 +18,21 @@ package modelgarden
 
 import (
 	"context"
-	"encoding/base64"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"os"
-	"regexp"
 	"sync"
 
 	"github.com/firebase/genkit/go/ai"
 	"github.com/firebase/genkit/go/genkit"
-	"github.com/firebase/genkit/go/plugins/internal/uri"
-	"github.com/invopop/jsonschema"
+	ant "github.com/firebase/genkit/go/plugins/internal/anthropic"
 
 	"github.com/anthropics/anthropic-sdk-go"
 	"github.com/anthropics/anthropic-sdk-go/vertex"
 )
 
 const (
-	MaxNumberOfTokens = 8192
-	ToolNameRegex     = `^[a-zA-Z0-9_-]{1,64}$`
+	provider = "vertexai"
 )
 
 type Anthropic struct {
@@ -95,8 +90,8 @@ func (a *Anthropic) Init(ctx context.Context, g *genkit.Genkit) (err error) {
 	a.initted = true
 	a.client = c
 
-	for name, mi := range anthropicModels {
-		defineAnthropicModel(g, a.client, name, mi)
+	for name, mi := range ant.AnthropicModels {
+		ant.DefineModel(g, a.client, provider, name, mi)
 	}
 
 	return nil
@@ -113,323 +108,12 @@ func (a *Anthropic) DefineModel(g *genkit.Genkit, name string, info *ai.ModelInf
 	var mi ai.ModelInfo
 	if info == nil {
 		var ok bool
-		mi, ok = anthropicModels[name]
+		mi, ok = ant.AnthropicModels[name]
 		if !ok {
 			return nil, fmt.Errorf("%s.DefineModel: called with unknown model %q and nil ModelInfo", provider, name)
 		}
 	} else {
 		mi = *info
 	}
-	return defineAnthropicModel(g, a.client, name, mi), nil
-}
-
-func defineAnthropicModel(g *genkit.Genkit, client anthropic.Client, name string, info ai.ModelInfo) ai.Model {
-	meta := &ai.ModelInfo{
-		Label:    provider + "-" + name,
-		Supports: info.Supports,
-		Versions: info.Versions,
-	}
-	return genkit.DefineModel(g, provider, name, meta, func(
-		ctx context.Context,
-		input *ai.ModelRequest,
-		cb func(context.Context, *ai.ModelResponseChunk) error,
-	) (*ai.ModelResponse, error) {
-		return anthropicGenerate(ctx, client, name, input, cb)
-	})
-}
-
-// generate function defines how a generate request is done in Anthropic models
-func anthropicGenerate(
-	ctx context.Context,
-	client anthropic.Client,
-	model string,
-	input *ai.ModelRequest,
-	cb func(context.Context, *ai.ModelResponseChunk) error,
-) (*ai.ModelResponse, error) {
-	req, err := toAnthropicRequest(model, input)
-	if err != nil {
-		return nil, fmt.Errorf("unable to generate anthropic request: %w", err)
-	}
-
-	// no streaming
-	if cb == nil {
-		msg, err := client.Messages.New(ctx, *req)
-		if err != nil {
-			return nil, err
-		}
-
-		r, err := anthropicToGenkitResponse(msg)
-		if err != nil {
-			return nil, err
-		}
-
-		r.Request = input
-		return r, nil
-	} else {
-		stream := client.Messages.NewStreaming(ctx, *req)
-		message := anthropic.Message{}
-		for stream.Next() {
-			event := stream.Current()
-			err := message.Accumulate(event)
-			if err != nil {
-				return nil, err
-			}
-
-			switch event := event.AsAny().(type) {
-			case anthropic.ContentBlockDeltaEvent:
-				cb(ctx, &ai.ModelResponseChunk{
-					Content: []*ai.Part{
-						{
-							Text: event.Delta.Text,
-						},
-					},
-				})
-			case anthropic.MessageStopEvent:
-				r, err := anthropicToGenkitResponse(&message)
-				if err != nil {
-					return nil, err
-				}
-				r.Request = input
-				return r, nil
-			}
-		}
-		if stream.Err() != nil {
-			return nil, stream.Err()
-		}
-	}
-
-	return nil, nil
-}
-
-func toAnthropicRole(role ai.Role) (anthropic.MessageParamRole, error) {
-	switch role {
-	case ai.RoleUser:
-		return anthropic.MessageParamRoleUser, nil
-	case ai.RoleModel:
-		return anthropic.MessageParamRoleAssistant, nil
-	case ai.RoleTool:
-		return anthropic.MessageParamRoleAssistant, nil
-	default:
-		return "", fmt.Errorf("unknown role given: %q", role)
-	}
-}
-
-// toAnthropicRequest translates [ai.ModelRequest] to an Anthropic request
-func toAnthropicRequest(model string, i *ai.ModelRequest) (*anthropic.MessageNewParams, error) {
-	messages := make([]anthropic.MessageParam, 0)
-
-	c, err := configFromRequest(i)
-	if err != nil {
-		return nil, err
-	}
-
-	// minimum required data to perform a request
-	req := anthropic.MessageNewParams{}
-	req.Model = anthropic.Model(model)
-	req.MaxTokens = int64(MaxNumberOfTokens)
-
-	if c.MaxOutputTokens != 0 {
-		req.MaxTokens = int64(c.MaxOutputTokens)
-	}
-	if c.Version != "" {
-		req.Model = anthropic.Model(c.Version)
-	}
-	if c.Temperature != 0 {
-		req.Temperature = anthropic.Float(c.Temperature)
-	}
-	if c.TopK != 0 {
-		req.TopK = anthropic.Int(int64(c.TopK))
-	}
-	if c.TopP != 0 {
-		req.TopP = anthropic.Float(float64(c.TopP))
-	}
-	if len(c.StopSequences) > 0 {
-		req.StopSequences = c.StopSequences
-	}
-
-	// configure system prompt (if given)
-	sysBlocks := []anthropic.TextBlockParam{}
-	for _, message := range i.Messages {
-		if message.Role == ai.RoleSystem {
-			// only text is supported for system messages
-			sysBlocks = append(sysBlocks, anthropic.TextBlockParam{Text: message.Text()})
-		} else if message.Content[len(message.Content)-1].IsToolResponse() {
-			// if the last message is a ToolResponse, the conversation must continue
-			// and the ToolResponse message must be sent as a user
-			// see: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#handling-tool-use-and-tool-result-content-blocks
-			parts, err := toAnthropicParts(message.Content)
-			if err != nil {
-				return nil, err
-			}
-			messages = append(messages, anthropic.NewUserMessage(parts...))
-		} else {
-			parts, err := toAnthropicParts(message.Content)
-			if err != nil {
-				return nil, err
-			}
-			role, err := toAnthropicRole(message.Role)
-			if err != nil {
-				return nil, err
-			}
-			messages = append(messages, anthropic.MessageParam{
-				Role:    role,
-				Content: parts,
-			})
-		}
-	}
-
-	req.System = sysBlocks
-	req.Messages = messages
-
-	tools, err := toAnthropicTools(i.Tools)
-	if err != nil {
-		return nil, err
-	}
-	req.Tools = tools
-
-	return &req, nil
-}
-
-// mapToStruct unmarshals a map[String]any to the expected type
-func mapToStruct(m map[string]any, v any) error {
-	jsonData, err := json.Marshal(m)
-	if err != nil {
-		return err
-	}
-	return json.Unmarshal(jsonData, v)
-}
-
-// configFromRequest converts any supported config type to [ai.GenerationCommonConfig]
-func configFromRequest(input *ai.ModelRequest) (*ai.GenerationCommonConfig, error) {
-	var result ai.GenerationCommonConfig
-
-	switch config := input.Config.(type) {
-	case ai.GenerationCommonConfig:
-		result = config
-	case *ai.GenerationCommonConfig:
-		result = *config
-	case map[string]any:
-		if err := mapToStruct(config, &result); err != nil {
-			return nil, err
-		}
-	case nil:
-		// Empty configuration is considered valid
-	default:
-		return nil, fmt.Errorf("unexpected config type: %T", input.Config)
-	}
-	return &result, nil
-}
-
-// toAnthropicTools translates [ai.ToolDefinition] to an anthropic.ToolParam type
-func toAnthropicTools(tools []*ai.ToolDefinition) ([]anthropic.ToolUnionParam, error) {
-	resp := make([]anthropic.ToolUnionParam, 0)
-	regex := regexp.MustCompile(ToolNameRegex)
-
-	for _, t := range tools {
-		if t.Name == "" {
-			return nil, fmt.Errorf("tool name is required")
-		}
-		if !regex.MatchString(t.Name) {
-			return nil, fmt.Errorf("tool name must match regex: %s", ToolNameRegex)
-		}
-
-		resp = append(resp, anthropic.ToolUnionParam{
-			OfTool: &anthropic.ToolParam{
-				Name:        t.Name,
-				Description: anthropic.String(t.Description),
-				InputSchema: toAnthropicSchema[map[string]any](),
-			},
-		})
-	}
-
-	return resp, nil
-}
-
-// toAnthropicSchema generates a JSON schema for the requested input type
-func toAnthropicSchema[T any]() anthropic.ToolInputSchemaParam {
-	reflector := jsonschema.Reflector{
-		AllowAdditionalProperties: true,
-		DoNotReference:            true,
-	}
-	var v T
-	schema := reflector.Reflect(v)
-	return anthropic.ToolInputSchemaParam{
-		Properties: schema.Properties,
-	}
-}
-
-// toAnthropicParts translates [ai.Part] to an anthropic.ContentBlockParamUnion type
-func toAnthropicParts(parts []*ai.Part) ([]anthropic.ContentBlockParamUnion, error) {
-	blocks := []anthropic.ContentBlockParamUnion{}
-
-	for _, p := range parts {
-		switch {
-		case p.IsText():
-			blocks = append(blocks, anthropic.NewTextBlock(p.Text))
-		case p.IsMedia():
-			contentType, data, _ := uri.Data(p)
-			blocks = append(blocks, anthropic.NewImageBlockBase64(contentType, base64.StdEncoding.EncodeToString(data)))
-		case p.IsData():
-			contentType, data, _ := uri.Data(p)
-			blocks = append(blocks, anthropic.NewImageBlockBase64(contentType, base64.RawStdEncoding.EncodeToString(data)))
-		case p.IsToolRequest():
-			toolReq := p.ToolRequest
-			blocks = append(blocks, anthropic.NewToolUseBlock(toolReq.Ref, toolReq.Input, toolReq.Name))
-		case p.IsToolResponse():
-			toolResp := p.ToolResponse
-			output, err := json.Marshal(toolResp.Output)
-			if err != nil {
-				return nil, fmt.Errorf("unable to parse tool response, err: %w", err)
-			}
-			blocks = append(blocks, anthropic.NewToolResultBlock(toolResp.Ref, string(output), false))
-		default:
-			return nil, errors.New("unknown part type in the request")
-		}
-	}
-
-	return blocks, nil
-}
-
-// anthropicToGenkitResponse translates an Anthropic Message to [ai.ModelResponse]
-func anthropicToGenkitResponse(m *anthropic.Message) (*ai.ModelResponse, error) {
-	r := ai.ModelResponse{}
-
-	switch m.StopReason {
-	case anthropic.StopReasonMaxTokens:
-		r.FinishReason = ai.FinishReasonLength
-	case anthropic.StopReasonStopSequence:
-		r.FinishReason = ai.FinishReasonStop
-	case anthropic.StopReasonEndTurn:
-		r.FinishReason = ai.FinishReasonStop
-	case anthropic.StopReasonToolUse:
-		r.FinishReason = ai.FinishReasonStop
-	default:
-		r.FinishReason = ai.FinishReasonUnknown
-	}
-
-	msg := &ai.Message{}
-	msg.Role = ai.RoleModel
-	for _, part := range m.Content {
-		var p *ai.Part
-		switch part.AsAny().(type) {
-		case anthropic.TextBlock:
-			p = ai.NewTextPart(string(part.Text))
-		case anthropic.ToolUseBlock:
-			p = ai.NewToolRequestPart(&ai.ToolRequest{
-				Ref:   part.ID,
-				Input: part.Input,
-				Name:  part.Name,
-			})
-		default:
-			return nil, fmt.Errorf("unknown part: %#v", part)
-		}
-		msg.Content = append(msg.Content, p)
-	}
-
-	r.Message = msg
-	r.Usage = &ai.GenerationUsage{
-		InputTokens:  int(m.Usage.InputTokens),
-		OutputTokens: int(m.Usage.OutputTokens),
-	}
-	return &r, nil
+	return ant.DefineModel(g, a.client, provider, name, mi), nil
 }
diff --git a/go/plugins/vertexai/modelgarden/anthropic_test.go b/go/plugins/vertexai/modelgarden/anthropic_test.go
deleted file mode 100644
index 8ce077453..000000000
--- a/go/plugins/vertexai/modelgarden/anthropic_test.go
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright 2025 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-// SPDX-License-Identifier: Apache-2.0
-
-package modelgarden
-
-import (
-	"testing"
-
-	"github.com/anthropics/anthropic-sdk-go"
-	"github.com/firebase/genkit/go/ai"
-)
-
-func TestAnthropic(t *testing.T) {
-	req := &ai.ModelRequest{
-		Config: &ai.GenerationCommonConfig{
-			MaxOutputTokens: MaxNumberOfTokens,
-			Temperature:     0.5,
-			TopK:            2,
-			TopP:            1,
-			Version:         "claude-version-3",
-			StopSequences:   []string{"tool_use"},
-		},
-		Messages: []*ai.Message{
-			ai.NewSystemTextMessage("greet the user"),
-			ai.NewUserTextMessage("hello Claude"),
-			ai.NewModelTextMessage("hello User"),
-		},
-		Tools: []*ai.ToolDefinition{
-			{
-				Description: "foo description",
-				InputSchema: map[string]any{},
-				Name:        "foo-tool",
-			},
-		},
-	}
-	t.Run("to anthropic request", func(t *testing.T) {
-		ar, err := toAnthropicRequest("claude-3.7-opus", req)
-		if err != nil {
-			t.Fatal(err)
-		}
-		cfg, _ := req.Config.(*ai.GenerationCommonConfig)
-		if ar.MaxTokens != int64(cfg.MaxOutputTokens) {
-			t.Errorf("want: %d, got: %d", int64(cfg.MaxOutputTokens), ar.MaxTokens)
-		}
-		if ar.Temperature.Value != cfg.Temperature {
-			t.Errorf("want: %f, got: %f", cfg.Temperature, ar.Temperature.Value)
-		}
-		if ar.TopK.Value != int64(cfg.TopK) {
-			t.Errorf("want: %d, got: %d", int64(cfg.TopK), ar.TopK.Value)
-		}
-		if ar.TopP.Value != float64(cfg.TopP) {
-			t.Errorf("want: %f, got: %f", float64(cfg.TopP), ar.TopP.Value)
-		}
-		if string(ar.Model) != cfg.Version {
-			t.Errorf("want: %q, got: %q", cfg.Version, ar.Model)
-		}
-		if ar.Tools == nil {
-			t.Errorf("expecting tools, got nil")
-		}
-		if ar.Tools[0].OfTool.Name != req.Tools[0].Name {
-			t.Errorf("want: %q, got: %q", req.Tools[0].Name, ar.Tools[0].OfTool.Name)
-		}
-		if ar.Tools[0].OfTool.Description.Value != req.Tools[0].Description {
-			t.Errorf("want: %q, got: %q", req.Tools[0].Name, ar.Tools[0].OfTool.Name)
-		}
-		if ar.Tools[0].OfTool.InputSchema.Properties == nil {
-			t.Errorf("expecting input schema, got nil")
-		}
-		if len(ar.Messages) == 0 {
-			t.Errorf("expecting messages, got empty")
-		}
-		if ar.System[0].Text == "" {
-			t.Errorf("expecting system message, got empty")
-		}
-		if len(ar.Messages) != 2 {
-			t.Errorf("expecting 2 messages, got: %d", len(ar.Messages))
-		}
-	})
-	t.Run("to anthropic role", func(t *testing.T) {
-		r, err := toAnthropicRole(ai.RoleModel)
-		if err != nil {
-			t.Error(err)
-		}
-		if r != anthropic.MessageParamRoleAssistant {
-			t.Errorf("want: %q, got: %q", anthropic.MessageParamRoleAssistant, r)
-		}
-		r, err = toAnthropicRole(ai.RoleUser)
-		if err != nil {
-			t.Error(err)
-		}
-		if r != anthropic.MessageParamRoleUser {
-			t.Errorf("want: %q, got: %q", anthropic.MessageParamRoleUser, r)
-		}
-		r, err = toAnthropicRole(ai.RoleSystem)
-		if err == nil {
-			t.Errorf("should have failed, got: %q", r)
-		}
-		r, err = toAnthropicRole(ai.RoleTool)
-		if err != nil {
-			t.Error(err)
-		}
-		if r != anthropic.MessageParamRoleAssistant {
-			t.Errorf("want: %q, got: %q", anthropic.MessageParamRoleAssistant, r)
-		}
-		r, err = toAnthropicRole("unknown")
-		if err == nil {
-			t.Errorf("should have failed, got: %q", r)
-		}
-	})
-}
diff --git a/go/samples/anthropic/main.go b/go/samples/anthropic/main.go
new file mode 100644
index 000000000..595568a38
--- /dev/null
+++ b/go/samples/anthropic/main.go
@@ -0,0 +1,32 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"context"
+	"log"
+
+	"github.com/firebase/genkit/go/genkit"
+	"github.com/firebase/genkit/go/plugins/anthropic"
+)
+
+func main() {
+	ctx := context.Background()
+
+	_, err := genkit.Init(ctx, genkit.WithPlugins(&anthropic.Anthropic{}))
+	if err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/go/samples/modelgarden/main.go b/go/samples/modelgarden/main.go
index 5ce005705..2c1b5fe7d 100644
--- a/go/samples/modelgarden/main.go
+++ b/go/samples/modelgarden/main.go
@@ -19,6 +19,7 @@ import (
 	"errors"
 	"log"
 
+	"github.com/anthropics/anthropic-sdk-go"
 	"github.com/firebase/genkit/go/ai"
 	"github.com/firebase/genkit/go/genkit"
 	"github.com/firebase/genkit/go/plugins/vertexai/modelgarden"
@@ -41,8 +42,8 @@ func main() {
 
 		resp, err := genkit.Generate(ctx, g,
 			ai.WithModel(m),
-			ai.WithConfig(&ai.GenerationCommonConfig{
-				Temperature: 1.0,
+			ai.WithConfig(&anthropic.MessageNewParams{
+				Temperature: anthropic.Float(1.0),
 			}),
 			ai.WithPrompt(`Tell a short joke about %s`, input))
 		if err != nil {