Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions cmd/tapes/serve/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const proxyLongDesc string = `Run the proxy server.
The proxy intercepts all requests and transparently forwards them to the
configured upstream URL, recording request/response conversation turns.

Supported provider types: anthropic, openai, ollama
Supported provider types: anthropic, openai, ollama, vertex

Optionally configure vector storage and embeddings of text content for "tapes search"
agentic functionality.`
Expand Down Expand Up @@ -109,7 +109,7 @@ func NewProxyCmd() *cobra.Command {
defaults := config.NewDefaultConfig()
cmd.Flags().StringVarP(&cmder.listen, "listen", "l", defaults.Proxy.Listen, "Address for proxy to listen on")
cmd.Flags().StringVarP(&cmder.upstream, "upstream", "u", defaults.Proxy.Upstream, "Upstream LLM provider URL")
cmd.Flags().StringVarP(&cmder.providerType, "provider", "p", defaults.Proxy.Provider, "LLM provider type (anthropic, openai, ollama)")
cmd.Flags().StringVarP(&cmder.providerType, "provider", "p", defaults.Proxy.Provider, "LLM provider type (anthropic, openai, ollama, vertex)")
cmd.Flags().StringVarP(&cmder.sqlitePath, "sqlite", "s", "", "Path to SQLite database (default: in-memory)")
cmd.Flags().StringVar(&cmder.vectorStoreProvider, "vector-store-provider", defaults.VectorStore.Provider, "Vector store provider type (e.g., chroma, sqlite)")
cmd.Flags().StringVar(&cmder.vectorStoreTarget, "vector-store-target", defaults.VectorStore.Target, "Vector store URL (e.g., http://localhost:8000)")
Expand Down
2 changes: 1 addition & 1 deletion cmd/tapes/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func NewServeCmd() *cobra.Command {
cmd.Flags().StringVarP(&cmder.proxyListen, "proxy-listen", "p", defaults.Proxy.Listen, "Address for proxy to listen on")
cmd.Flags().StringVarP(&cmder.apiListen, "api-listen", "a", defaults.API.Listen, "Address for API server to listen on")
cmd.Flags().StringVarP(&cmder.upstream, "upstream", "u", defaults.Proxy.Upstream, "Upstream LLM provider URL")
cmd.Flags().StringVar(&cmder.providerType, "provider", defaults.Proxy.Provider, "LLM provider type (anthropic, openai, ollama)")
cmd.Flags().StringVar(&cmder.providerType, "provider", defaults.Proxy.Provider, "LLM provider type (anthropic, openai, ollama, vertex)")
cmd.Flags().StringVarP(&cmder.sqlitePath, "sqlite", "s", "", "Path to SQLite database (e.g., ./tapes.sqlite, in-memory)")
cmd.Flags().StringVar(&cmder.vectorStoreProvider, "vector-store-provider", defaults.VectorStore.Provider, "Vector store provider type (e.g., chroma, sqlite)")
cmd.Flags().StringVar(&cmder.vectorStoreTarget, "vector-store-target", defaults.VectorStore.Target, "Vector store target filepath for sqlite or URL for vector store service (e.g., http://localhost:8000, ./db.sqlite)")
Expand Down
2 changes: 1 addition & 1 deletion pkg/llm/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var ErrStreamingNotImplemented = errors.New("streaming not implemented for this
// Each provider implementation knows how to parse its specific
// API format into the internal representation.
type Provider interface {
// Name returns the canonical provider name (e.g., "anthropic", "openai", "ollama")
// Name returns the canonical provider name (e.g., "anthropic", "openai", "ollama", "vertex")
Name() string

// DefaultStreaming reports whether this provider streams responses by default
Expand Down
6 changes: 5 additions & 1 deletion pkg/llm/provider/supported.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import (
"github.com/papercomputeco/tapes/pkg/llm/provider/anthropic"
"github.com/papercomputeco/tapes/pkg/llm/provider/ollama"
"github.com/papercomputeco/tapes/pkg/llm/provider/openai"
"github.com/papercomputeco/tapes/pkg/llm/provider/vertex"
)

// Supported provider type constants
const (
Anthropic = "anthropic"
OpenAI = "openai"
Ollama = "ollama"
Vertex = "vertex"
)

// SupportedProviders returns the list of all supported provider type names.
func SupportedProviders() []string {
return []string{Anthropic, OpenAI, Ollama}
return []string{Anthropic, OpenAI, Ollama, Vertex}
}

// New creates a new Provider instance for the given provider type.
Expand All @@ -30,6 +32,8 @@ func New(providerType string) (Provider, error) {
return openai.New(), nil
case Ollama:
return ollama.New(), nil
case Vertex:
return vertex.New(), nil
default:
return nil, fmt.Errorf("unknown provider type: %q (supported: %v)", providerType, SupportedProviders())
}
Expand Down
58 changes: 58 additions & 0 deletions pkg/llm/provider/vertex/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package vertex

// vertexRequest represents a Vertex AI request for Anthropic Claude models.
// This is the same as the Anthropic Messages API format with two differences:
// - "model" is omitted (it is specified in the Vertex AI endpoint URL)
// - "anthropic_version" is included in the body (not as a header)
type vertexRequest struct {
AnthropicVersion string `json:"anthropic_version,omitempty"`
Model string `json:"model,omitempty"`
Messages []vertexMessage `json:"messages"`
System any `json:"system,omitempty"`
MaxTokens int `json:"max_tokens"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
TopK *int `json:"top_k,omitempty"`
Stop []string `json:"stop_sequences,omitempty"`
Stream *bool `json:"stream,omitempty"`
}

type vertexMessage struct {
Role string `json:"role"`

// Union type: can be "string" or "[]vertexContentBlock"
Content any `json:"content"`
}

type vertexContentBlock struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Source *vertexSource `json:"source,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input map[string]any `json:"input,omitempty"`
}

type vertexSource struct {
Type string `json:"type"`
MediaType string `json:"media_type"`
Data string `json:"data"`
}

// vertexResponse represents a Vertex AI response for Anthropic Claude models.
// The response format is identical to the Anthropic Messages API.
type vertexResponse struct {
ID string `json:"id"`
Type string `json:"type"`
Role string `json:"role"`
Content []vertexContentBlock `json:"content"`
Model string `json:"model"`
StopReason string `json:"stop_reason"`
StopSequence *string `json:"stop_sequence,omitempty"`
Usage *vertexUsage `json:"usage,omitempty"`
}

type vertexUsage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
191 changes: 191 additions & 0 deletions pkg/llm/provider/vertex/vertex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Package vertex implements the Provider interface for Anthropic Claude models
// accessed through Google Cloud's Vertex AI platform.
//
// The Vertex AI Claude API is nearly identical to the Anthropic Messages API,
// with two key differences:
// - "model" is not passed in the request body (it is specified in the Vertex AI endpoint URL)
// - "anthropic_version" is passed in the request body (rather than as a header)
package vertex

import (
"encoding/json"
"strings"
"time"

"github.com/papercomputeco/tapes/pkg/llm"
)

// Provider implements the Provider interface for Vertex AI (Anthropic Claude).
type Provider struct{}

// New
func New() *Provider { return &Provider{} }

// Name
func (p *Provider) Name() string {
return "vertex"
}

// DefaultStreaming is false - Vertex AI requires explicit "stream": true.
func (p *Provider) DefaultStreaming() bool {
return false
}

func (p *Provider) ParseRequest(payload []byte) (*llm.ChatRequest, error) {
var req vertexRequest
if err := json.Unmarshal(payload, &req); err != nil {
return nil, err
}

system := parseVertexSystem(req.System)
messages := make([]llm.Message, 0, len(req.Messages))
for _, msg := range req.Messages {
converted := llm.Message{Role: msg.Role}

switch content := msg.Content.(type) {
case string:
converted.Content = []llm.ContentBlock{{Type: "text", Text: content}}
case []any:
for _, item := range content {
if block, ok := item.(map[string]any); ok {
cb := llm.ContentBlock{}
if t, ok := block["type"].(string); ok {
cb.Type = t
}
if text, ok := block["text"].(string); ok {
cb.Text = text
}
if source, ok := block["source"].(map[string]any); ok {
if mt, ok := source["media_type"].(string); ok {
cb.MediaType = mt
}
if data, ok := source["data"].(string); ok {
cb.ImageBase64 = data
}
}

// Tool use
if id, ok := block["id"].(string); ok {
cb.ToolUseID = id
}
if name, ok := block["name"].(string); ok {
cb.ToolName = name
}
if input, ok := block["input"].(map[string]any); ok {
cb.ToolInput = input
}
converted.Content = append(converted.Content, cb)
}
}
}

messages = append(messages, converted)
}

extra := map[string]any{}
if req.AnthropicVersion != "" {
extra["anthropic_version"] = req.AnthropicVersion
}

result := &llm.ChatRequest{
Model: req.Model,
Messages: messages,
System: system,
MaxTokens: &req.MaxTokens,
Temperature: req.Temperature,
TopP: req.TopP,
TopK: req.TopK,
Stop: req.Stop,
Stream: req.Stream,
RawRequest: payload,
}

if len(extra) > 0 {
result.Extra = extra
}

return result, nil
}

func parseVertexSystem(system any) string {
if system == nil {
return ""
}

switch value := system.(type) {
case string:
return value
case []any:
var builder strings.Builder
for _, item := range value {
block, ok := item.(map[string]any)
if !ok {
continue
}
blockType, _ := block["type"].(string)
text, _ := block["text"].(string)
if blockType == "text" && text != "" {
if builder.Len() > 0 {
builder.WriteString("\n")
}
builder.WriteString(text)
}
}
return builder.String()
default:
return ""
}
}

func (p *Provider) ParseResponse(payload []byte) (*llm.ChatResponse, error) {
var resp vertexResponse
if err := json.Unmarshal(payload, &resp); err != nil {
return nil, err
}

content := make([]llm.ContentBlock, 0, len(resp.Content))
for _, block := range resp.Content {
cb := llm.ContentBlock{Type: block.Type}
switch block.Type {
case "text":
cb.Text = block.Text
case "tool_use":
cb.ToolUseID = block.ID
cb.ToolName = block.Name
cb.ToolInput = block.Input
}
content = append(content, cb)
}

var usage *llm.Usage
if resp.Usage != nil {
usage = &llm.Usage{
PromptTokens: resp.Usage.InputTokens,
CompletionTokens: resp.Usage.OutputTokens,
TotalTokens: resp.Usage.InputTokens + resp.Usage.OutputTokens,
}
}

result := &llm.ChatResponse{
Model: resp.Model,
Message: llm.Message{
Role: resp.Role,
Content: content,
},
Done: true,
StopReason: resp.StopReason,
Usage: usage,
CreatedAt: time.Now(),
RawResponse: payload,
Extra: map[string]any{
"id": resp.ID,
"type": resp.Type,
},
}

return result, nil
}

func (p *Provider) ParseStreamChunk(_ []byte) (*llm.StreamChunk, error) {
panic("not implemented")
}
13 changes: 13 additions & 0 deletions pkg/llm/provider/vertex/vertex_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package vertex_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestVertex(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Vertex Provider Suite")
}
Loading
Loading