mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 09:13:36 +08:00
@@ -121,6 +121,7 @@ type openAIChatMessage struct {
|
||||
}
|
||||
|
||||
func buildOpenAIMessages(reqMessages []ai.Message, modelName string, baseURL string) []openAIChatMessage {
|
||||
reqMessages = normalizeOpenAISystemMessageOrder(reqMessages)
|
||||
messages := make([]openAIChatMessage, len(reqMessages))
|
||||
replayReasoningContent := shouldReplayReasoningContent(modelName, baseURL)
|
||||
for i, m := range reqMessages {
|
||||
@@ -172,6 +173,41 @@ func buildOpenAIMessages(reqMessages []ai.Message, modelName string, baseURL str
|
||||
return messages
|
||||
}
|
||||
|
||||
// normalizeOpenAISystemMessageOrder keeps strict OpenAI-compatible endpoints
|
||||
// from rejecting a historical system message that appears after a user,
|
||||
// assistant, or tool message. It preserves the relative order within both
|
||||
// groups so tool-call sequences remain intact.
|
||||
func normalizeOpenAISystemMessageOrder(messages []ai.Message) []ai.Message {
|
||||
seenNonSystemMessage := false
|
||||
needsReorder := false
|
||||
for _, message := range messages {
|
||||
if message.Role == "system" {
|
||||
if seenNonSystemMessage {
|
||||
needsReorder = true
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
seenNonSystemMessage = true
|
||||
}
|
||||
if !needsReorder {
|
||||
return messages
|
||||
}
|
||||
|
||||
reordered := make([]ai.Message, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
if message.Role == "system" {
|
||||
reordered = append(reordered, message)
|
||||
}
|
||||
}
|
||||
for _, message := range messages {
|
||||
if message.Role != "system" {
|
||||
reordered = append(reordered, message)
|
||||
}
|
||||
}
|
||||
return reordered
|
||||
}
|
||||
|
||||
func attachReasoningContent(msg *openAIChatMessage, source ai.Message, enabled bool) {
|
||||
if enabled && source.Role == "assistant" && source.ReasoningContent != "" {
|
||||
msg.ReasoningContent = source.ReasoningContent
|
||||
|
||||
@@ -224,6 +224,124 @@ func TestOpenAIProviderChatUsesRequestMaxTokens(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIProviderChatMovesSystemMessagesToRequestPrefix(t *testing.T) {
|
||||
var received openAIChatRequest
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(&received); err != nil {
|
||||
t.Fatalf("decode request body failed: %v", err)
|
||||
}
|
||||
|
||||
seenNonSystemMessage := false
|
||||
for _, message := range received.Messages {
|
||||
if message.Role == "system" {
|
||||
if seenNonSystemMessage {
|
||||
http.Error(w, `{"error":{"message":"System message must be at the beginning."}}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
seenNonSystemMessage = true
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"pong"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
providerInstance, err := NewOpenAIProvider(ai.ProviderConfig{
|
||||
Type: "openai",
|
||||
APIKey: "sk-test",
|
||||
BaseURL: server.URL,
|
||||
Model: "gpt-chat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create provider failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = providerInstance.Chat(context.Background(), ai.ChatRequest{
|
||||
Messages: []ai.Message{
|
||||
{Role: "user", Content: "first question"},
|
||||
{Role: "system", Content: "follow the product rules"},
|
||||
{Role: "assistant", Content: "first answer"},
|
||||
{Role: "system", Content: "follow the workspace rules"},
|
||||
{Role: "user", Content: "next question"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("chat should normalize system message ordering, got %v", err)
|
||||
}
|
||||
|
||||
gotRoles := make([]string, len(received.Messages))
|
||||
for index, message := range received.Messages {
|
||||
gotRoles[index] = message.Role
|
||||
}
|
||||
wantRoles := []string{"system", "system", "user", "assistant", "user"}
|
||||
if len(gotRoles) != len(wantRoles) {
|
||||
t.Fatalf("expected roles %v, got %v", wantRoles, gotRoles)
|
||||
}
|
||||
for index, want := range wantRoles {
|
||||
if gotRoles[index] != want {
|
||||
t.Fatalf("expected roles %v, got %v", wantRoles, gotRoles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIProviderChatStreamMovesSystemMessagesToRequestPrefix(t *testing.T) {
|
||||
var received openAIChatRequest
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(&received); err != nil {
|
||||
t.Fatalf("decode request body failed: %v", err)
|
||||
}
|
||||
|
||||
seenNonSystemMessage := false
|
||||
for _, message := range received.Messages {
|
||||
if message.Role == "system" {
|
||||
if seenNonSystemMessage {
|
||||
http.Error(w, `{"error":{"message":"System message must be at the beginning."}}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
seenNonSystemMessage = true
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"pong\"}}]}\n\ndata: [DONE]\n\n"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
providerInstance, err := NewOpenAIProvider(ai.ProviderConfig{
|
||||
Type: "openai",
|
||||
APIKey: "sk-test",
|
||||
BaseURL: server.URL,
|
||||
Model: "gpt-chat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create provider failed: %v", err)
|
||||
}
|
||||
|
||||
var response string
|
||||
err = providerInstance.ChatStream(context.Background(), ai.ChatRequest{
|
||||
Messages: []ai.Message{
|
||||
{Role: "user", Content: "first question"},
|
||||
{Role: "system", Content: "follow the product rules"},
|
||||
},
|
||||
}, func(chunk ai.StreamChunk) {
|
||||
response += chunk.Content
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("chat stream should normalize system message ordering, got %v", err)
|
||||
}
|
||||
if response != "pong" {
|
||||
t.Fatalf("expected streamed content pong, got %q", response)
|
||||
}
|
||||
if len(received.Messages) != 2 || received.Messages[0].Role != "system" || received.Messages[1].Role != "user" {
|
||||
t.Fatalf("expected system message before user message, got %#v", received.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIProviderChatRetriesWithoutImagesOnHTTP400(t *testing.T) {
|
||||
requestCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user