Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(go): add modelgarden and anthropic support #1902

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
31c90e7
add model garden + claude models
hugoaguirre Feb 6, 2025
5b2a639
Merge branch 'main' into haguirre/addModelGarden
hugoaguirre Feb 7, 2025
09089e1
add client management and plugin arch
hugoaguirre Feb 8, 2025
745b038
add test cases and minor refactor
hugoaguirre Feb 10, 2025
d910186
add claude client and test cases (genkit wiring missing)
hugoaguirre Feb 11, 2025
966355e
wiring: system and user text messages + basic conf
hugoaguirre Feb 11, 2025
4e257f5
test: add model version test
hugoaguirre Feb 11, 2025
817bc06
tidy: added constants and docs
hugoaguirre Feb 11, 2025
1caa09c
docs: client.go docs
hugoaguirre Feb 11, 2025
78f759c
feat: add support to claude models
hugoaguirre Feb 12, 2025
4532142
use state.clients instead of standalone variable
hugoaguirre Feb 12, 2025
c27a307
docs: refine anthropic docs
hugoaguirre Feb 12, 2025
6458f88
use the least req params
hugoaguirre Feb 12, 2025
b425e02
fix: minor fixes and docs
hugoaguirre Feb 13, 2025
b95226e
draft: add tooling, media and system prompts features to anthropic
hugoaguirre Feb 13, 2025
946b360
feat: add system and user roles + media
hugoaguirre Feb 13, 2025
90ca4a6
feat: add media support and test cases
hugoaguirre Feb 13, 2025
c4590aa
Merge branch 'main' into haguirre/addModelGarden
hugoaguirre Feb 13, 2025
cf917e1
misc: tool flow complete, disabled tests -- prepare for refactor
hugoaguirre Feb 18, 2025
5a0758e
feat: add tools support (no response yet)
hugoaguirre Feb 19, 2025
e137900
docs: refined logs
hugoaguirre Feb 19, 2025
e77de85
feat: added streaming support
hugoaguirre Feb 19, 2025
6493ef0
test: add tools streaming test
hugoaguirre Feb 19, 2025
f59eb86
fix: refactor to independant packages
hugoaguirre Feb 19, 2025
28825e8
fix: update modelgarden sample
hugoaguirre Feb 19, 2025
04cfeaf
Merge branch 'main' into haguirre/addModelGarden
hugoaguirre Feb 19, 2025
aed1aea
add go.mod and go.sum
hugoaguirre Feb 19, 2025
0482fc4
docs: updated license in samples
hugoaguirre Feb 24, 2025
d3b29c6
feat: add support for claude-3-7-sonnet
hugoaguirre Feb 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 361 additions & 0 deletions go/plugins/vertexai/modelgarden/anthropic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

package modelgarden

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"regexp"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/internal/gemini"
"github.com/firebase/genkit/go/plugins/internal/uri"
"github.com/invopop/jsonschema"

"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/vertex"
)

const (
MaxNumberOfTokens = 8192
ToolNameRegex = `^[a-zA-Z0-9_-]{1,64}$`
)

// supported anthropic models
var AnthropicModels = map[string]ai.ModelInfo{
"claude-3-5-sonnet-v2": {
Label: "Vertex AI Model Garden - Claude 3.5 Sonnet",
Supports: &gemini.Multimodal,
Versions: []string{"claude-3-5-sonnet-v2@20241022"},
},
"claude-3-5-sonnet": {
Label: "Vertex AI Model Garden - Claude 3.5 Sonnet",
Supports: &gemini.Multimodal,
Versions: []string{"claude-3-5-sonnet@20240620"},
},
"claude-3-sonnet": {
Label: "Vertex AI Model Garden - Claude 3 Sonnet",
Supports: &gemini.Multimodal,
Versions: []string{"claude-3-sonnet@20240229"},
},
"claude-3-haiku": {
Label: "Vertex AI Model Garden - Claude 3 Haiku",
Supports: &gemini.Multimodal,
Versions: []string{"claude-3-haiku@20240307"},
},
"claude-3-opus": {
Label: "Vertex AI Model Garden - Claude 3 Opus",
Supports: &gemini.Multimodal,
Versions: []string{"claude-3-opus@20240229"},
},
}

// AnthropicClientConfig is the required configuration to create an Anthropic
// client
type AnthropicClientConfig struct {
Location string
Project string
}

// AnthropicClient is a mirror struct of Anthropic's client but implements
// [Client] interface
type AnthropicClient struct {
*anthropic.Client
}

// Anthropic defines how an Anthropic client is created
var Anthropic = func(config any) (Client, error) {
cfg, ok := config.(*AnthropicClientConfig)
if !ok {
return nil, fmt.Errorf("invalid config for Anthropic %T", config)
}
c := anthropic.NewClient(
vertex.WithGoogleAuth(context.Background(), cfg.Location, cfg.Project),
)

return &AnthropicClient{c}, nil
}

// DefineModel adds the model to the registry
func (a *AnthropicClient) DefineModel(g *genkit.Genkit, name string, info *ai.ModelInfo) (ai.Model, error) {
var mi ai.ModelInfo
if info == nil {
var ok bool
mi, ok = AnthropicModels[name]
if !ok {
return nil, fmt.Errorf("%s.DefineModel: called with unknown model %q and nil ModelInfo", AnthropicProvider, name)
}
} else {
mi = *info
}
return defineModel(g, a, name, mi), nil
}

func defineModel(g *genkit.Genkit, client *AnthropicClient, name string, info ai.ModelInfo) ai.Model {
meta := &ai.ModelInfo{
Label: AnthropicProvider + "-" + name,
Supports: info.Supports,
Versions: info.Versions,
}
return genkit.DefineModel(g, AnthropicProvider, 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 *AnthropicClient,
model string,
input *ai.ModelRequest,
cb func(context.Context, *ai.ModelResponseChunk) error,
) (*ai.ModelResponse, error) {
req, err := toAnthropicRequest(model, input)
if err != nil {
panic(fmt.Sprintf("unable to generate anthropic request: %v", err))
}

// no streaming
if cb == nil {
msg, err := client.Messages.New(ctx, req)
if err != nil {
return nil, err
}

r := toGenkitResponse(msg)
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 {
panic(err)
}

switch event := event.AsUnion().(type) {
case anthropic.ContentBlockDeltaEvent:
cb(ctx, &ai.ModelResponseChunk{
Content: []*ai.Part{
{
Text: event.Delta.Text,
},
},
})
case anthropic.MessageStopEvent:
r := toGenkitResponse(&message)
r.Request = input
return r, nil
}
}
if stream.Err() != nil {
panic(stream.Err())
}
}

return nil, nil
}

func toAnthropicRole(role ai.Role) anthropic.MessageParamRole {
switch role {
case ai.RoleUser:
return anthropic.MessageParamRoleUser
case ai.RoleModel:
return anthropic.MessageParamRoleAssistant
case ai.RoleTool:
return anthropic.MessageParamRoleAssistant
default:
panic(fmt.Sprintf("unsupported role type: %v", role))
}
}

// toAnthropicRequest translates [ai.ModelRequest] to an Anthropic request
func toAnthropicRequest(model string, i *ai.ModelRequest) (anthropic.MessageNewParams, error) {
req := anthropic.MessageNewParams{}
messages := make([]anthropic.MessageParam, 0)

// minimum required data to perform a request
req.Model = anthropic.F(anthropic.Model(model))
req.MaxTokens = anthropic.F(int64(MaxNumberOfTokens))

if c, ok := i.Config.(*ai.GenerationCommonConfig); ok && c != nil {
if c.MaxOutputTokens != 0 {
req.MaxTokens = anthropic.F(int64(c.MaxOutputTokens))
}
req.Model = anthropic.F(anthropic.Model(model))
if c.Version != "" {
req.Model = anthropic.F(anthropic.Model(c.Version))
}
if c.Temperature != 0 {
req.Temperature = anthropic.F(c.Temperature)
}
if c.TopK != 0 {
req.TopK = anthropic.F(int64(c.TopK))
}
if c.TopP != 0 {
req.TopP = anthropic.F(float64(c.TopP))
}
if len(c.StopSequences) > 0 {
req.StopSequences = anthropic.F(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.NewTextBlock(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 := convertParts(message.Content)
if err != nil {
return req, err
}
messages = append(messages, anthropic.NewUserMessage(parts...))
} else {
// handle the rest of the messages
parts, err := convertParts(message.Content)
if err != nil {
return req, err
}
messages = append(messages, anthropic.MessageParam{
Role: anthropic.F(toAnthropicRole(message.Role)),
Content: anthropic.F(parts),
})
}
}

req.System = anthropic.F(sysBlocks)
req.Messages = anthropic.F(messages)

// check tools
tools, err := convertTools(i.Tools)
if err != nil {
return req, err
}
req.Tools = anthropic.F(tools)

return req, nil
}

// convertTools translates [ai.ToolDefinition] to an anthropic.ToolParam type
func convertTools(tools []*ai.ToolDefinition) ([]anthropic.ToolParam, error) {
resp := make([]anthropic.ToolParam, 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.ToolParam{
Name: anthropic.F(t.Name),
Description: anthropic.F(t.Description),
InputSchema: anthropic.F(generateSchema[map[string]any]()),
})
}

return resp, nil
}

func generateSchema[T any]() interface{} {
reflector := jsonschema.Reflector{
AllowAdditionalProperties: false,
DoNotReference: true,
}
var v T
return reflector.Reflect(v)
}

// convertParts translates [ai.Part] to an anthropic.ContentBlockParamUnion type
func convertParts(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():
// todo: what is this? is this related to ContentBlocks?
panic("data content is unsupported by anthropic models")
case p.IsToolRequest():
toolReq := p.ToolRequest
blocks = append(blocks, anthropic.NewToolUseBlockParam(toolReq.Ref, toolReq.Name, toolReq.Input))
case p.IsToolResponse():
toolResp := p.ToolResponse
output, err := json.Marshal(toolResp.Output)
if err != nil {
panic(fmt.Sprintf("unable to parse tool response: %v", err))
}
blocks = append(blocks, anthropic.NewToolResultBlock(toolResp.Ref, string(output), false))
default:
panic("unknown part type in the request")
}
}

return blocks, nil
}

// toGenkitResponse translates an Anthropic Message to [ai.ModelResponse]
func toGenkitResponse(m *anthropic.Message) *ai.ModelResponse {
r := &ai.ModelResponse{}

switch m.StopReason {
case anthropic.MessageStopReasonMaxTokens:
r.FinishReason = ai.FinishReasonLength
case anthropic.MessageStopReasonStopSequence:
r.FinishReason = ai.FinishReasonStop
case anthropic.MessageStopReasonEndTurn:
r.FinishReason = ai.FinishReasonStop
case anthropic.MessageStopReasonToolUse:
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.Type {
case anthropic.ContentBlockTypeText:
p = ai.NewTextPart(string(part.Text))
case anthropic.ContentBlockTypeToolUse:
p = ai.NewToolRequestPart(&ai.ToolRequest{
Ref: part.ID,
Input: part.Input,
Name: part.Name,
})
default:
panic(fmt.Sprintf("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
}
Loading
Loading