diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 29d66df5..f7d0f18a 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -119,7 +119,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte switch itemType { case "message": - if strings.EqualFold(itemRole, "system") { + if strings.EqualFold(itemRole, "system") || strings.EqualFold(itemRole, "developer") { if contentArray := item.Get("content"); contentArray.Exists() { systemInstr := []byte(`{"parts":[]}`) if systemInstructionResult := gjson.GetBytes(out, "systemInstruction"); systemInstructionResult.Exists() { diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go index 35418689..0693b63d 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go @@ -55,6 +55,80 @@ func TestConvertOpenAIResponsesRequestToGemini_ReasoningSignatureCompatibility(t } } +func TestConvertOpenAIResponsesRequestToGemini_SystemAndDeveloperRoles(t *testing.T) { + tests := []struct { + name string + role string + wantText string + }{ + { + name: "system role", + role: "system", + wantText: "System message text", + }, + { + name: "developer role", + role: "developer", + wantText: "Developer message text", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := []byte(`{ + "instructions": "Be a helpful assistant", + "input": [ + { + "type": "message", + "role": "` + tt.role + `", + "content": [ + { + "type": "input_text", + "text": "` + tt.wantText + `" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Hello" + } + ] + } + ] + }`) + + output := ConvertOpenAIResponsesRequestToGemini("gemini-3.5-flash", input, false) + result := gjson.ParseBytes(output) + + systemInstruction := result.Get("systemInstruction") + if !systemInstruction.Exists() { + t.Fatalf("systemInstruction missing. Output: %s", output) + } + parts := systemInstruction.Get("parts") + if got := parts.Get("#").Int(); got != 2 { + t.Fatalf("systemInstruction parts = %d, want 2. Output: %s", got, output) + } + if got := parts.Get("0.text").String(); got != "Be a helpful assistant" { + t.Fatalf("first systemInstruction part = %q, want %q. Output: %s", got, "Be a helpful assistant", output) + } + if got := parts.Get("1.text").String(); got != tt.wantText { + t.Fatalf("second systemInstruction part = %q, want %q. Output: %s", got, tt.wantText, output) + } + + result.Get("contents").ForEach(func(_, value gjson.Result) bool { + if role := value.Get("role").String(); role == tt.role { + t.Fatalf("role %q leaked into contents array. Output: %s", tt.role, output) + } + return true + }) + }) + } +} + func validResponsesGPTReasoningSignature() string { raw := make([]byte, 1+8+16+16+32) raw[0] = 0x80