From 31a52f71910c3d987c33d9a7922907d8ed832863 Mon Sep 17 00:00:00 2001 From: Kayvan Sylvan Date: Tue, 30 Dec 2025 09:43:22 -0800 Subject: [PATCH] refactor: extract message conversion logic to `toMessages` method in VertexAI client - Extract message conversion into dedicated `toMessages` helper method - Add proper role handling for system, user, and assistant messages - Prepend system content to first user message per Anthropic format - Enforce user/assistant message alternation with placeholder messages - Skip empty messages during conversion processing - Concatenate multiple text blocks in response output - Add validation for empty message arrays before sending - Handle edge case when only system content is provided --- internal/plugins/ai/vertexai/vertexai.go | 90 +++++++++++++++++++++--- 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/internal/plugins/ai/vertexai/vertexai.go b/internal/plugins/ai/vertexai/vertexai.go index ef9cf6ae..0860be0f 100644 --- a/internal/plugins/ai/vertexai/vertexai.go +++ b/internal/plugins/ai/vertexai/vertexai.go @@ -3,6 +3,7 @@ package vertexai import ( "context" "fmt" + "strings" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/vertex" @@ -74,9 +75,9 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o } // 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)) + anthropicMessages := c.toMessages(msgs) + if len(anthropicMessages) == 0 { + return "", fmt.Errorf("no valid messages to send") } // Create the request @@ -92,11 +93,18 @@ func (c *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o } // Extract text from response - if len(response.Content) > 0 { - return response.Content[0].Text, nil + var textParts []string + for _, block := range response.Content { + if block.Type == "text" && block.Text != "" { + textParts = append(textParts, block.Text) + } } - return "", fmt.Errorf("no content in response") + if len(textParts) == 0 { + return "", fmt.Errorf("no content in response") + } + + return strings.Join(textParts, ""), nil } func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan string) error { @@ -109,9 +117,9 @@ func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha 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)) + anthropicMessages := c.toMessages(msgs) + if len(anthropicMessages) == 0 { + return fmt.Errorf("no valid messages to send") } // Create streaming request @@ -133,6 +141,70 @@ func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha return stream.Err() } +func (c *Client) toMessages(msgs []*chat.ChatCompletionMessage) []anthropic.MessageParam { + // Convert messages to Anthropic format with proper role handling + // - System messages become part of the first user message + // - Messages must alternate user/assistant + // - Skip empty messages + + var anthropicMessages []anthropic.MessageParam + var systemContent string + + isFirstUserMessage := true + lastRoleWasUser := false + + for _, msg := range msgs { + if strings.TrimSpace(msg.Content) == "" { + continue // Skip empty messages + } + + switch msg.Role { + case chat.ChatMessageRoleSystem: + // Accumulate system content to prepend to first user message + if systemContent != "" { + systemContent += "\\n" + msg.Content + } else { + systemContent = msg.Content + } + case chat.ChatMessageRoleUser: + userContent := msg.Content + if isFirstUserMessage && systemContent != "" { + userContent = systemContent + "\\n\\n" + userContent + isFirstUserMessage = false + } + if lastRoleWasUser { + // Enforce alternation: add a minimal assistant message + anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(anthropic.NewTextBlock("Okay."))) + } + anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(anthropic.NewTextBlock(userContent))) + lastRoleWasUser = true + case chat.ChatMessageRoleAssistant: + // If first message is assistant and we have system content, prepend user message + if isFirstUserMessage && systemContent != "" { + anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(anthropic.NewTextBlock(systemContent))) + lastRoleWasUser = true + isFirstUserMessage = false + } else if !lastRoleWasUser && len(anthropicMessages) > 0 { + // Enforce alternation: add a minimal user message + anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(anthropic.NewTextBlock("Hi"))) + lastRoleWasUser = true + } + anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(msg.Content))) + lastRoleWasUser = false + default: + // Other roles are ignored for Anthropic's message structure + continue + } + } + + // If only system content was provided, create a user message with it + if len(anthropicMessages) == 0 && systemContent != "" { + anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(anthropic.NewTextBlock(systemContent))) + } + + return anthropicMessages +} + func (c *Client) NeedsRawMode(modelName string) bool { return false }