diff --git a/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go b/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go index e74ca5d0ae..fef3dc545c 100644 --- a/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go +++ b/relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go @@ -131,7 +131,8 @@ func GeminiGenerateContentRequestToOpenAIChat(ctx context.Context, geminiRequest if len(toolCalls) > 0 { message.SetToolCalls(toolCalls) - } else if len(mediaContents) == 1 && mediaContents[0].Type == "text" { + } + if len(mediaContents) == 1 && mediaContents[0].Type == "text" { message.Content = mediaContents[0].Text } else if len(mediaContents) > 0 { message.SetMediaContent(mediaContents) diff --git a/relaykit/relayconvert/request_registry_test.go b/relaykit/relayconvert/request_registry_test.go index 87f9ba0c1f..d971763e91 100644 --- a/relaykit/relayconvert/request_registry_test.go +++ b/relaykit/relayconvert/request_registry_test.go @@ -1110,3 +1110,52 @@ func mustMarshalRequestJSON(t *testing.T, value any) []byte { require.NoError(t, err) return raw } + +func TestGeminiToOpenAIChatPreservesTextAlongsideToolCalls(t *testing.T) { + const text = "I will check the weather." + call := dto.GeminiPart{FunctionCall: &dto.FunctionCall{ + ID: "call_weather", FunctionName: "get_weather", Arguments: map[string]any{"city": "Paris"}, + }} + for _, tt := range []struct { + name string + parts []dto.GeminiPart + wantText []string + wantCalls int + }{ + {name: "text and call", parts: []dto.GeminiPart{{Text: text}, call}, wantText: []string{text}, wantCalls: 1}, + {name: "text around call", parts: []dto.GeminiPart{{Text: text}, call, {Text: "Please wait."}}, wantText: []string{text, "Please wait."}, wantCalls: 1}, + {name: "call only", parts: []dto.GeminiPart{call}, wantCalls: 1}, + {name: "text only", parts: []dto.GeminiPart{{Text: text}}, wantText: []string{text}}, + } { + t.Run(tt.name, func(t *testing.T) { + request := &dto.GeminiChatRequest{Contents: []dto.GeminiChatContent{{Role: "model", Parts: tt.parts}}} + result, err := ConvertRequest(context.Background(), &convmeta.Values{ + UpstreamModelName: "gpt-4o", ChannelMetaAttached: true, + }, types.RelayFormatOpenAI, request) + require.NoError(t, err) + converted, ok := result.Value.(*dto.GeneralOpenAIRequest) + require.True(t, ok) + // Verify the JSON sent upstream, including custom DTO serialization. + body, err := kitutil.Marshal(converted) + require.NoError(t, err) + var wire dto.GeneralOpenAIRequest + require.NoError(t, kitutil.Unmarshal(body, &wire)) + require.Len(t, wire.Messages, 1) + assistant := wire.Messages[0] + assert.Equal(t, "assistant", assistant.Role) + content := assistant.ParseContent() + require.Len(t, content, len(tt.wantText)) + for i, want := range tt.wantText { + assert.Equal(t, "text", content[i].Type) + assert.Equal(t, want, content[i].Text) + } + calls := assistant.ParseToolCalls() + require.Len(t, calls, tt.wantCalls) + if tt.wantCalls > 0 { + assert.Equal(t, "call_weather", calls[0].ID) + assert.Equal(t, "get_weather", calls[0].Function.Name) + assert.JSONEq(t, `{"city":"Paris"}`, calls[0].Function.Arguments) + } + }) + } +}