Skip to content

Claude Sonnet 4 and Opus 4 Support for VertexAI Provider #317

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

Closed
wants to merge 4 commits into from
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ OpenCode supports a variety of AI models from different providers:

- Gemini 2.5
- Gemini 2.5 Flash
- Claude Sonnet 4
- Claude Opus 4

## Usage

Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ require (
github.com/stretchr/testify v1.10.0
)

require (
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/api v0.215.0 // indirect
)

require (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.13.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
Expand Down Expand Up @@ -250,6 +252,8 @@ github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
Expand Down Expand Up @@ -289,6 +293,8 @@ golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -329,11 +335,15 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0=
google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY=
google.golang.org/genai v1.3.0 h1:tXhPJF30skOjnnDY7ZnjK3q7IKy4PuAlEA0fk7uEaEI=
google.golang.org/genai v1.3.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
Expand Down
2 changes: 1 addition & 1 deletion internal/llm/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,6 @@ func init() {
maps.Copy(SupportedModels, AzureModels)
maps.Copy(SupportedModels, OpenRouterModels)
maps.Copy(SupportedModels, XAIModels)
maps.Copy(SupportedModels, VertexAIGeminiModels)
maps.Copy(SupportedModels, VertexAIModels)
maps.Copy(SupportedModels, CopilotModels)
}
36 changes: 34 additions & 2 deletions internal/llm/models/vertexai.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package models
const (
ProviderVertexAI ModelProvider = "vertexai"

// Models
// Gemini Models
VertexAIGemini25Flash ModelID = "vertexai.gemini-2.5-flash"
VertexAIGemini25 ModelID = "vertexai.gemini-2.5"

// Claude Models
VertexAIClaude4Sonnet ModelID = "vertexai.claude-sonnet-4"
VertexAIClaude4Opus ModelID = "vertexai.claude-opus-4"
)

var VertexAIGeminiModels = map[ModelID]Model{
var VertexAIModels = map[ModelID]Model{
VertexAIGemini25Flash: {
ID: VertexAIGemini25Flash,
Name: "VertexAI: Gemini 2.5 Flash",
Expand All @@ -35,4 +39,32 @@ var VertexAIGeminiModels = map[ModelID]Model{
DefaultMaxTokens: GeminiModels[Gemini25].DefaultMaxTokens,
SupportsAttachments: true,
},
VertexAIClaude4Sonnet: {
ID: VertexAIClaude4Sonnet,
Name: "VertexAI: Claude Sonnet 4",
Provider: ProviderVertexAI,
APIModel: "claude-sonnet-4",
CostPer1MIn: AnthropicModels[Claude4Sonnet].CostPer1MIn,
CostPer1MInCached: AnthropicModels[Claude4Sonnet].CostPer1MInCached,
CostPer1MOut: AnthropicModels[Claude4Sonnet].CostPer1MOut,
CostPer1MOutCached: AnthropicModels[Claude4Sonnet].CostPer1MOutCached,
ContextWindow: AnthropicModels[Claude4Sonnet].ContextWindow,
DefaultMaxTokens: AnthropicModels[Claude4Sonnet].DefaultMaxTokens,
CanReason: AnthropicModels[Claude4Sonnet].CanReason,
SupportsAttachments: AnthropicModels[Claude4Sonnet].SupportsAttachments,
},
VertexAIClaude4Opus: {
ID: VertexAIClaude4Opus,
Name: "VertexAI: Claude Opus 4",
Provider: ProviderVertexAI,
APIModel: "claude-opus-4",
CostPer1MIn: AnthropicModels[Claude4Opus].CostPer1MIn,
CostPer1MInCached: AnthropicModels[Claude4Opus].CostPer1MInCached,
CostPer1MOut: AnthropicModels[Claude4Opus].CostPer1MOut,
CostPer1MOutCached: AnthropicModels[Claude4Opus].CostPer1MOutCached,
ContextWindow: AnthropicModels[Claude4Opus].ContextWindow,
DefaultMaxTokens: AnthropicModels[Claude4Opus].DefaultMaxTokens,
CanReason: AnthropicModels[Claude4Opus].CanReason,
SupportsAttachments: AnthropicModels[Claude4Opus].SupportsAttachments,
},
}
162 changes: 162 additions & 0 deletions internal/llm/models/vertexai_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package models

import (
"strings"
"testing"
)

func TestVertexAIClaudeModels(t *testing.T) {
// Test that Claude Sonnet 4 model is correctly defined
claude4Sonnet, exists := SupportedModels[VertexAIClaude4Sonnet]
if !exists {
t.Errorf("VertexAI Claude Sonnet 4 model not found in SupportedModels")
return
}

// Verify model properties
if claude4Sonnet.ID != VertexAIClaude4Sonnet {
t.Errorf("Expected ID %s, got %s", VertexAIClaude4Sonnet, claude4Sonnet.ID)
}
if claude4Sonnet.Name != "VertexAI: Claude Sonnet 4" {
t.Errorf("Expected name 'VertexAI: Claude Sonnet 4', got %s", claude4Sonnet.Name)
}
if claude4Sonnet.Provider != ProviderVertexAI {
t.Errorf("Expected provider %s, got %s", ProviderVertexAI, claude4Sonnet.Provider)
}
if claude4Sonnet.APIModel != "claude-sonnet-4" {
t.Errorf("Expected API model 'claude-sonnet-4', got %s", claude4Sonnet.APIModel)
}
if !claude4Sonnet.CanReason {
t.Errorf("Expected Claude Sonnet 4 to support reasoning")
}
if !claude4Sonnet.SupportsAttachments {
t.Errorf("Expected Claude Sonnet 4 to support attachments")
}

// Test that Claude Opus 4 model is correctly defined
claude4Opus, exists := SupportedModels[VertexAIClaude4Opus]
if !exists {
t.Errorf("VertexAI Claude Opus 4 model not found in SupportedModels")
return
}

// Verify model properties
if claude4Opus.ID != VertexAIClaude4Opus {
t.Errorf("Expected ID %s, got %s", VertexAIClaude4Opus, claude4Opus.ID)
}
if claude4Opus.Name != "VertexAI: Claude Opus 4" {
t.Errorf("Expected name 'VertexAI: Claude Opus 4', got %s", claude4Opus.Name)
}
if claude4Opus.Provider != ProviderVertexAI {
t.Errorf("Expected provider %s, got %s", ProviderVertexAI, claude4Opus.Provider)
}
if claude4Opus.APIModel != "claude-opus-4" {
t.Errorf("Expected API model 'claude-opus-4', got %s", claude4Opus.APIModel)
}
if !claude4Opus.SupportsAttachments {
t.Errorf("Expected Claude Opus 4 to support attachments")
}

// Check reasoning capability - should match the Anthropic model
anthropicOpusModel := AnthropicModels[Claude4Opus]
if claude4Opus.CanReason != anthropicOpusModel.CanReason {
t.Errorf("Expected CanReason to match Anthropic model: %v, got %v", anthropicOpusModel.CanReason, claude4Opus.CanReason)
}

// Test that pricing is inherited correctly from Anthropic models
anthropicSonnet := AnthropicModels[Claude4Sonnet]
if claude4Sonnet.CostPer1MIn != anthropicSonnet.CostPer1MIn {
t.Errorf("Expected inherited input cost %f, got %f", anthropicSonnet.CostPer1MIn, claude4Sonnet.CostPer1MIn)
}
if claude4Sonnet.ContextWindow != anthropicSonnet.ContextWindow {
t.Errorf("Expected inherited context window %d, got %d", anthropicSonnet.ContextWindow, claude4Sonnet.ContextWindow)
}

anthropicOpus := AnthropicModels[Claude4Opus]
if claude4Opus.CostPer1MIn != anthropicOpus.CostPer1MIn {
t.Errorf("Expected inherited input cost %f, got %f", anthropicOpus.CostPer1MIn, claude4Opus.CostPer1MIn)
}
if claude4Opus.ContextWindow != anthropicOpus.ContextWindow {
t.Errorf("Expected inherited context window %d, got %d", anthropicOpus.ContextWindow, claude4Opus.ContextWindow)
}
}

func TestVertexAIProviderPriority(t *testing.T) {
// Test that VertexAI provider is included in the popularity rankings
priority, exists := ProviderPopularity[ProviderVertexAI]
if !exists {
t.Errorf("VertexAI provider not found in ProviderPopularity")
return
}

// VertexAI should have a reasonable priority (not 0)
if priority <= 0 {
t.Errorf("Expected positive priority for VertexAI provider, got %d", priority)
}
}

// Test model routing for all defined models
func TestVertexAI_AllModelRouting(t *testing.T) {
claudeModels := []ModelID{
VertexAIClaude4Sonnet,
VertexAIClaude4Opus,
}

geminiModels := []ModelID{
VertexAIGemini25Flash,
VertexAIGemini25,
}

// Test Claude models route correctly
for _, modelID := range claudeModels {
t.Run(string(modelID), func(t *testing.T) {
model := SupportedModels[modelID]
if !strings.HasPrefix(model.APIModel, "claude-") {
t.Errorf("Claude model %s should have 'claude-' prefix, got %s", modelID, model.APIModel)
}
})
}

// Test Gemini models route correctly
for _, modelID := range geminiModels {
t.Run(string(modelID), func(t *testing.T) {
model := SupportedModels[modelID]
if strings.HasPrefix(model.APIModel, "claude-") {
t.Errorf("Gemini model %s should not have 'claude-' prefix, got %s", modelID, model.APIModel)
}
})
}
}

// Test model definitions for required fields
func TestVertexAI_ClaudeModelDefinitions(t *testing.T) {
claudeModels := []ModelID{
VertexAIClaude4Sonnet,
VertexAIClaude4Opus,
}

for _, modelID := range claudeModels {
t.Run(string(modelID), func(t *testing.T) {
model := SupportedModels[modelID]

// Verify required fields
if model.APIModel == "" {
t.Errorf("API model should not be empty")
}
if model.Name == "" {
t.Errorf("Display name should not be empty")
}
if model.ContextWindow <= 0 {
t.Errorf("Context window should be positive, got %d", model.ContextWindow)
}
if model.DefaultMaxTokens <= 0 {
t.Errorf("Max output tokens should be positive, got %d", model.DefaultMaxTokens)
}

// Verify Claude-specific requirements
if !model.SupportsAttachments {
t.Errorf("Claude models should support attachments")
}
})
}
}
Loading