feat(ai): add VertexAI provider for Claude models

Add support for Google Cloud Vertex AI as a provider to access Claude models
using Application Default Credentials (ADC). This allows users to route their
Fabric requests through Google Cloud Platform instead of directly to Anthropic,
enabling billing through GCP.

Features:
- Support for Claude models (Sonnet 4.5, Opus 4.5, Haiku 4.5, etc.) via Vertex AI
- Uses Google ADC for authentication (no API keys required)
- Configurable project ID and region (defaults to 'global' for cost optimization)
- Full support for streaming and non-streaming requests
- Implements complete ai.Vendor interface

Configuration:
- VERTEXAI_PROJECT_ID: GCP project ID (required)
- VERTEXAI_REGION: Vertex AI region (optional, defaults to 'global')

Closes #1570
This commit is contained in:
Rodaddy
2025-12-29 14:33:25 -05:00
parent 45d06f8854
commit 3cb0be03c7
4 changed files with 153 additions and 0 deletions

2
go.mod
View File

@@ -58,9 +58,11 @@ require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
)

11
go.sum
View File

@@ -81,6 +81,8 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -94,6 +96,11 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
@@ -248,6 +255,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -312,6 +321,8 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=

View File

@@ -23,6 +23,7 @@ import (
"github.com/danielmiessler/fabric/internal/plugins/ai/openai"
"github.com/danielmiessler/fabric/internal/plugins/ai/openai_compatible"
"github.com/danielmiessler/fabric/internal/plugins/ai/perplexity"
"github.com/danielmiessler/fabric/internal/plugins/ai/vertexai"
"github.com/danielmiessler/fabric/internal/plugins/strategy"
"github.com/samber/lo"
@@ -101,6 +102,7 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
azure.NewClient(),
gemini.NewClient(),
anthropic.NewClient(),
vertexai.NewClient(),
lmstudio.NewClient(),
exolab.NewClient(),
perplexity.NewClient(), // Added Perplexity client

View File

@@ -0,0 +1,138 @@
package vertexai
import (
"context"
"fmt"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/vertex"
"github.com/danielmiessler/fabric/internal/chat"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/plugins"
)
const (
cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
defaultRegion = "global"
maxTokens = 4096
)
// NewClient creates a new Vertex AI client for accessing Claude models via Google Cloud
func NewClient() (ret *Client) {
vendorName := "VertexAI"
ret = &Client{}
ret.PluginBase = &plugins.PluginBase{
Name: vendorName,
EnvNamePrefix: plugins.BuildEnvVariablePrefix(vendorName),
ConfigureCustom: ret.configure,
}
ret.ProjectID = ret.AddSetupQuestion("Project ID", true)
ret.Region = ret.AddSetupQuestion("Region", false)
ret.Region.Value = defaultRegion
return
}
// Client implements the ai.Vendor interface for Google Cloud Vertex AI with Anthropic models
type Client struct {
*plugins.PluginBase
ProjectID *plugins.SetupQuestion
Region *plugins.SetupQuestion
client *anthropic.Client
}
func (c *Client) configure() error {
ctx := context.Background()
projectID := c.ProjectID.Value
region := c.Region.Value
// Initialize Anthropic client for Claude models via Vertex AI using Google ADC
vertexOpt := vertex.WithGoogleAuth(ctx, region, projectID, cloudPlatformScope)
client := anthropic.NewClient(vertexOpt)
c.client = &client
return nil
}
func (c *Client) ListModels() ([]string, error) {
// Return Claude models available on Vertex AI
return []string{
string(anthropic.ModelClaudeSonnet4_5),
string(anthropic.ModelClaudeOpus4_5),
string(anthropic.ModelClaudeHaiku4_5),
string(anthropic.ModelClaude3_7SonnetLatest),
string(anthropic.ModelClaude3_5HaikuLatest),
}, nil
}
func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions) (string, error) {
if c.client == nil {
return "", fmt.Errorf("VertexAI client not initialized")
}
// Convert chat messages to Anthropic format
anthropicMessages := make([]anthropic.MessageParam, len(msgs))
for i, msg := range msgs {
anthropicMessages[i] = anthropic.NewUserMessage(anthropic.NewTextBlock(msg.Content))
}
// Create the request
response, err := c.client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.Model(opts.Model),
MaxTokens: int64(maxTokens),
Messages: anthropicMessages,
Temperature: anthropic.Opt(opts.Temperature),
})
if err != nil {
return "", err
}
// Extract text from response
if len(response.Content) > 0 {
return response.Content[0].Text, nil
}
return "", fmt.Errorf("no content in response")
}
func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan string) error {
if c.client == nil {
close(channel)
return fmt.Errorf("VertexAI client not initialized")
}
defer close(channel)
ctx := context.Background()
// Convert chat messages to Anthropic format
anthropicMessages := make([]anthropic.MessageParam, len(msgs))
for i, msg := range msgs {
anthropicMessages[i] = anthropic.NewUserMessage(anthropic.NewTextBlock(msg.Content))
}
// Create streaming request
stream := c.client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{
Model: anthropic.Model(opts.Model),
MaxTokens: int64(maxTokens),
Messages: anthropicMessages,
Temperature: anthropic.Opt(opts.Temperature),
})
// Process stream
for stream.Next() {
event := stream.Current()
if event.Delta.Text != "" {
channel <- event.Delta.Text
}
}
return stream.Err()
}
func (c *Client) NeedsRawMode(modelName string) bool {
return false
}