fix: don't persist ephemeral sessions in multi-model chat completions (#17637)

This commit is contained in:
euvre
2026-07-31 19:12:24 +08:00
committed by GitHub
parent e1f724b691
commit 29e5181095
4 changed files with 98 additions and 9 deletions

View File

@@ -127,6 +127,8 @@ type ChatCompletionsRequest struct {
LLMID string `json:"llm_id,omitempty"`
PassAllHistoryMessages *bool `json:"pass_all_history_messages,omitempty"`
PassAllHistory *bool `json:"pass_all_history,omitempty"`
StoreHistoryMessages *bool `json:"store_history_messages,omitempty"`
StoreHistory *bool `json:"store_history,omitempty"`
Legacy bool `json:"legacy,omitempty"`
Stream *bool `json:"stream"`
Temperature *float64 `json:"temperature,omitempty"`
@@ -205,12 +207,26 @@ func (h *ChatSessionHandler) ChatCompletions(c *gin.Context) {
passAllHistory = *req.PassAllHistoryMessages
}
// Resolve store_history from either alias (default true, mirrors Python)
storeHistory := true
if req.StoreHistory != nil {
storeHistory = *req.StoreHistory
}
if req.StoreHistoryMessages != nil {
storeHistory = *req.StoreHistoryMessages
}
if !storeHistory && !passAllHistory {
common.ErrorWithCode(c, common.CodeDataError, "`pass_all_history_messages` must be true when `store_history_messages` is false.")
return
}
// Remove known keys from rawBody; what remains is passthrough kwargs
knownKeys := []string{
"chat_id", "session_id", "conversation_id",
"messages", "question", "files",
"llm_id",
"pass_all_history_messages", "pass_all_history",
"store_history_messages", "store_history",
"legacy", "stream",
"temperature", "top_p", "frequency_penalty", "presence_penalty", "max_tokens",
}
@@ -241,7 +257,7 @@ func (h *ChatSessionHandler) ChatCompletions(c *gin.Context) {
req.ChatID, sessionID,
req.Messages, req.Question, req.Files,
req.LLMID, genConfig, kwargs,
passAllHistory, req.Legacy,
passAllHistory, storeHistory, req.Legacy,
true, streamChan,
)
}()
@@ -261,7 +277,7 @@ func (h *ChatSessionHandler) ChatCompletions(c *gin.Context) {
req.ChatID, sessionID,
req.Messages, req.Question, req.Files,
req.LLMID, genConfig, kwargs,
passAllHistory, req.Legacy,
passAllHistory, storeHistory, req.Legacy,
false, nil,
)
if err != nil {

View File

@@ -321,6 +321,7 @@ func (s *ChatChannelService) HandleIncomingMessage(ctx context.Context, msg Chat
nil,
map[string]interface{}{"quote": false},
false,
true,
false,
false,
nil,

View File

@@ -1449,7 +1449,7 @@ func (s *ChatSessionService) ChatCompletions(
chatID string, sessionID string,
messages []map[string]interface{}, question string, files []interface{},
llmID string, genConfig map[string]interface{}, kwargs map[string]interface{},
passAllHistory bool, legacy bool,
passAllHistory bool, storeHistory bool, legacy bool,
stream bool, streamChan chan<- string,
) (map[string]interface{}, error) {
if ctx == nil {
@@ -1505,7 +1505,7 @@ func (s *ChatSessionService) ChatCompletions(
return fail(common.NewCodedError(common.CodeDataError, "Session does not belong to this chat!"))
}
} else {
session, err = s.createSessionForCompletion(ctx, chatID, dialog, userID)
session, err = s.createSessionForCompletion(ctx, chatID, dialog, userID, storeHistory)
if err != nil {
return fail(err)
}
@@ -1669,7 +1669,7 @@ func (s *ChatSessionService) ChatCompletions(
sendOrCancel(fmt.Sprintf("data:%s\n\n", marshalJSONWithSpaces(wrapper)))
// Persist session state (matches Python's update_by_id after loop)
if session != nil {
if session != nil && storeHistory {
s.updateSessionMessages(ctx, session, s.getSessionMessagesAsSlice(session), reference)
}
} else {
@@ -1679,7 +1679,9 @@ func (s *ChatSessionService) ChatCompletions(
if chatID != "" {
result["chat_id"] = chatID
}
s.updateSessionMessages(ctx, session, s.getSessionMessagesAsSlice(session), reference)
if storeHistory {
s.updateSessionMessages(ctx, session, s.getSessionMessagesAsSlice(session), reference)
}
return sanitizeJSONFloats(result).(map[string]interface{}), nil
}
ans["id"] = messageID
@@ -1830,8 +1832,7 @@ func (s *ChatSessionService) buildDefaultCompletionDialog(tenantID string) *enti
}
}
func (s *ChatSessionService) createSessionForCompletion(ctx context.Context, chatID string, dialog *entity.Chat, userID string) (*entity.ChatSession, error) {
newID := utility.GenerateUUID()
func (s *ChatSessionService) createSessionForCompletion(ctx context.Context, chatID string, dialog *entity.Chat, userID string, saveSession bool) (*entity.ChatSession, error) {
name := "New session"
prologue := "Hi! I'm your assistant. What can I do for you?"
@@ -1848,13 +1849,18 @@ func (s *ChatSessionService) createSessionForCompletion(ctx context.Context, cha
refJSON, _ := json.Marshal([]interface{}{})
session := &entity.ChatSession{
ID: newID,
DialogID: chatID,
Name: &name,
Message: msgJSON,
UserID: &userID,
Reference: refJSON,
}
if !saveSession {
// Multi-model comparison sends store_history_messages=false; the
// ephemeral session must not be persisted (mirrors Python).
return session, nil
}
session.ID = utility.GenerateUUID()
if err := s.chatSessionDAO.Create(ctx, dao.DB, session); err != nil {
return nil, err
}

View File

@@ -930,6 +930,7 @@ func TestChatCompletionsPassesRequestUserIDToPipeline(t *testing.T) {
nil,
nil,
false,
true,
false,
false,
nil,
@@ -1010,6 +1011,7 @@ func TestChatCompletionsStreamFinalCarriesDecoratedReference(t *testing.T) {
nil,
nil,
false,
true,
false,
true,
streamChan,
@@ -1106,6 +1108,7 @@ func TestChatCompletionsModelIDOverrideUsesModelResolver(t *testing.T) {
nil,
nil,
false,
true,
false,
false,
nil,
@@ -1121,6 +1124,69 @@ func TestChatCompletionsModelIDOverrideUsesModelResolver(t *testing.T) {
}
}
// Multi-model comparison sends store_history_messages=false; the ephemeral
// session must never be persisted, otherwise every model card adds a
// "New session" row to the session list.
func TestChatCompletionsStoreHistoryFalseDoesNotPersistSession(t *testing.T) {
store := newFakeSessionStore()
store.dialogs["dialog-1"] = &entity.Chat{
ID: "dialog-1",
TenantID: "tenant-owner",
LLMID: "chat@factory",
LLMSetting: entity.JSONMap{},
PromptConfig: entity.JSONMap{
"parameters": []interface{}{},
"prologue": "Welcome!",
},
}
store.dialogExists["tenant-owner|dialog-1"] = true
pipeline := &fakePipeline{
resultChan: makeResultChan(
AsyncChatResult{Answer: "ok", Final: true, Reference: map[string]interface{}{"chunks": []interface{}{}}},
),
}
svc := &ChatSessionService{
chatSessionDAO: store,
userTenantDAO: &fakeTenantStore{tenantIDs: []string{"tenant-owner"}},
pipeline: pipeline,
}
result, err := svc.ChatCompletions(
context.Background(),
"user-1",
"dialog-1",
"",
[]map[string]interface{}{{"role": "user", "content": "hi"}},
"",
nil,
"",
nil,
nil,
true,
false,
false,
false,
nil,
)
if err != nil {
t.Fatalf("ChatCompletions failed: %v", err)
}
if result == nil {
t.Fatal("expected a non-nil answer")
}
if len(store.createCalled) != 0 {
t.Fatalf("session must not be created when store_history_messages=false, got %d creates", len(store.createCalled))
}
if len(store.updateCalled) != 0 {
t.Fatalf("session must not be updated when store_history_messages=false, got %d updates", len(store.updateCalled))
}
if len(store.sessions) != 0 {
t.Fatalf("no session should be stored, got %d", len(store.sessions))
}
}
func TestCompletion_EmptyMessages(t *testing.T) {
svc := &ChatSessionService{
chatSessionDAO: &fakeSessionStore{},