diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go index 8fb82ef4..09b680c7 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go @@ -89,7 +89,7 @@ func responsesInputToChatMessages(instructions string, inputRaw json.RawMessage) return nil, fmt.Errorf("parse responses input item: %w", err) } - role := rawString(item["role"]) + role := chatCompletionsBridgeRole(rawString(item["role"])) itemType := rawString(item["type"]) switch itemType { case "function_call": @@ -130,9 +130,6 @@ func responsesInputToChatMessages(instructions string, inputRaw json.RawMessage) continue } - if role == "" { - role = "user" - } content := item["content"] if len(bytesTrimSpace(content)) == 0 { if text := rawString(item["text"]); text != "" { @@ -152,6 +149,17 @@ func responsesInputToChatMessages(instructions string, inputRaw json.RawMessage) return messages, nil } +func chatCompletionsBridgeRole(role string) string { + trimmed := strings.TrimSpace(role) + if trimmed == "" { + return "user" + } + if strings.EqualFold(trimmed, "developer") { + return "system" + } + return role +} + func responsesContentToChatContent(raw json.RawMessage, role string) (json.RawMessage, error) { raw = bytesTrimSpace(raw) if len(raw) == 0 || string(raw) == "null" { diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_test.go b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_test.go new file mode 100644 index 00000000..3e55e23a --- /dev/null +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_test.go @@ -0,0 +1,82 @@ +package apicompat + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResponsesInputToChatMessages_DeveloperRoleMapsToSystem(t *testing.T) { + messages, err := responsesInputToChatMessages("", json.RawMessage(`[{"role":"developer","content":"follow project instructions"}]`)) + require.NoError(t, err) + require.Len(t, messages, 1) + + assert.Equal(t, "system", messages[0].Role) + assert.JSONEq(t, `"follow project instructions"`, string(messages[0].Content)) +} + +func TestResponsesInputToChatMessages_KeepsChatCompletionRoles(t *testing.T) { + input := json.RawMessage(`[ + {"role":"system","content":"system message"}, + {"role":"user","content":"user message"}, + {"role":"assistant","content":"assistant message"}, + {"role":"tool","content":"tool message"} + ]`) + + messages, err := responsesInputToChatMessages("", input) + require.NoError(t, err) + require.Len(t, messages, 4) + + assert.Equal(t, []string{"system", "user", "assistant", "tool"}, chatMessageRoles(messages)) +} + +func TestResponsesInputToChatMessages_EmptyRoleFallsBackToUser(t *testing.T) { + messages, err := responsesInputToChatMessages("", json.RawMessage(`[{"role":"","content":"hello"}]`)) + require.NoError(t, err) + require.Len(t, messages, 1) + + assert.Equal(t, "user", messages[0].Role) +} + +func TestResponsesInputToChatMessages_DeveloperRoleTrimAndCaseInsensitive(t *testing.T) { + input := json.RawMessage(`[ + {"role":" Developer ","content":"one"}, + {"role":"\tDEVELOPER\n","content":"two"} + ]`) + + messages, err := responsesInputToChatMessages("", input) + require.NoError(t, err) + require.Len(t, messages, 2) + + assert.Equal(t, []string{"system", "system"}, chatMessageRoles(messages)) +} + +func TestResponsesToChatCompletionsRequest_InstructionsAndInputDeveloperRole(t *testing.T) { + req := &ResponsesRequest{ + Model: "gpt-4o", + Instructions: "Use concise answers.", + Input: json.RawMessage(`[ + {"role":"developer","content":[{"type":"input_text","text":"Prefer JSON."}]}, + {"role":"user","content":"Hello"} + ]`), + } + + out, err := ResponsesToChatCompletionsRequest(req) + require.NoError(t, err) + require.Len(t, out.Messages, 3) + + assert.Equal(t, []string{"system", "system", "user"}, chatMessageRoles(out.Messages)) + assert.JSONEq(t, `"Use concise answers."`, string(out.Messages[0].Content)) + assert.JSONEq(t, `"Prefer JSON."`, string(out.Messages[1].Content)) + assert.JSONEq(t, `"Hello"`, string(out.Messages[2].Content)) +} + +func chatMessageRoles(messages []ChatMessage) []string { + roles := make([]string, 0, len(messages)) + for _, message := range messages { + roles = append(roles, message.Role) + } + return roles +}