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
15 changes: 15 additions & 0 deletions relay/channel/vertex/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ func removeFunctionResponseID(request *dto.GeminiChatRequest) {
}

func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
if a.RequestMode == RequestModeGemini {
// Gemini upstreams reject the Anthropic Messages body (anthropic_version,
// messages, max_tokens). The /v1/messages request must be converted to
// Gemini generateContent, same as the /v1/chat/completions path.
result, err := service.ConvertRequest(c, info, types.RelayFormatGemini, request)
if err != nil {
return nil, err
}
geminiRequest, ok := result.Value.(*dto.GeminiChatRequest)
if !ok {
return nil, fmt.Errorf("expected Gemini generateContent request, got %T", result.Value)
}
c.Set("request_model", request.Model)
return geminiRequest, nil
}
if v, ok := claudeModelMap[info.UpstreamModelName]; ok {
c.Set("request_model", v)
} else {
Expand Down
99 changes: 99 additions & 0 deletions relay/channel/vertex/adaptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package vertex

import (
"net/http/httptest"
"testing"

"github.com/QuantumNous/new-api/common"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func newTestClaudeRequest() *dto.ClaudeRequest {
maxTokens := uint(16)
return &dto.ClaudeRequest{
Model: "gemini-2.5-pro",
MaxTokens: &maxTokens,
Messages: []dto.ClaudeMessage{
{Role: "user", Content: "hi"},
},
}
}

// A Vertex channel whose upstream is Gemini must convert the incoming Anthropic
// Messages body to Gemini generateContent. Forwarding the raw Anthropic body to
// the generateContent URL makes Vertex reject anthropic_version/messages/max_tokens
// with HTTP 400 (issue #6715). The channel test dispatches through this same
// ConvertClaudeRequest for the Anthropic endpoint, so this also fixes the test.
func TestConvertClaudeRequestByRequestMode(t *testing.T) {
gin.SetMode(gin.TestMode)

cases := []struct {
name string
requestMode int
wantKeys []string
absentKeys []string
}{
{
name: "gemini upstream converts to generateContent",
requestMode: RequestModeGemini,
wantKeys: []string{"contents", "generationConfig"},
absentKeys: []string{"anthropic_version", "messages", "max_tokens"},
},
{
name: "claude upstream keeps anthropic messages body",
requestMode: RequestModeClaude,
wantKeys: []string{"anthropic_version", "messages", "max_tokens"},
absentKeys: []string{"contents", "generationConfig"},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
info := &relaycommon.RelayInfo{
RelayFormat: types.RelayFormatClaude,
ChannelMeta: &relaycommon.ChannelMeta{
UpstreamModelName: "gemini-2.5-pro",
},
}
adaptor := &Adaptor{RequestMode: tc.requestMode}

converted, err := adaptor.ConvertClaudeRequest(c, info, newTestClaudeRequest())
require.NoError(t, err)
require.NotNil(t, converted)

raw, err := common.Marshal(converted)
require.NoError(t, err)

var body map[string]any
require.NoError(t, common.Unmarshal(raw, &body))
for _, key := range tc.wantKeys {
assert.Contains(t, body, key, "expected key %q in converted body", key)
}
for _, key := range tc.absentKeys {
assert.NotContains(t, body, key, "unexpected key %q in converted body", key)
}

switch tc.requestMode {
case RequestModeGemini:
geminiReq, ok := converted.(*dto.GeminiChatRequest)
require.True(t, ok, "expected *dto.GeminiChatRequest, got %T", converted)
require.Len(t, geminiReq.Contents, 1)
require.Len(t, geminiReq.Contents[0].Parts, 1)
assert.Equal(t, "hi", geminiReq.Contents[0].Parts[0].Text)
require.NotNil(t, geminiReq.GenerationConfig.MaxOutputTokens)
assert.Equal(t, uint(16), *geminiReq.GenerationConfig.MaxOutputTokens)
case RequestModeClaude:
claudeReq, ok := converted.(*VertexAIClaudeRequest)
require.True(t, ok, "expected *VertexAIClaudeRequest, got %T", converted)
assert.Equal(t, anthropicVersion, claudeReq.AnthropicVersion)
}
})
}
}