From dd5a0c52136a0fdfc2e76c8873cc2603ffc53f48 Mon Sep 17 00:00:00 2001 From: CJACK Date: Fri, 1 May 2026 22:27:59 +0800 Subject: [PATCH] refactor: update and standardize current input file continuation prompt instructions --- docs/prompt-compatibility.md | 4 +-- .../httpapi/openai/chat/chat_history_test.go | 6 ++-- .../openai/chat/vercel_prepare_test.go | 4 +-- .../openai/history/current_input_file.go | 4 +-- internal/httpapi/openai/history_split_test.go | 28 +++++++++---------- .../chatHistory/ChatHistoryContainer.jsx | 12 ++++++-- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/docs/prompt-compatibility.md b/docs/prompt-compatibility.md index f3538ff..58c2c6c 100644 --- a/docs/prompt-compatibility.md +++ b/docs/prompt-compatibility.md @@ -249,7 +249,7 @@ OpenAI 文件相关实现: 兼容层现在只保留 `current_input_file` 这一种拆分方式;旧的 `history_split` 已废弃,只保留为兼容旧配置的字段,不再参与请求处理。 -- `current_input_file` 默认开启;它用于把“完整上下文”合并进 `DS2API_HISTORY.txt` 上下文文件。当最新 user turn 的纯文本长度达到 `current_input_file.min_chars`(默认 `0`)时,兼容层会上传一个文件名为 `DS2API_HISTORY.txt` 的上下文文件。文件内容会先做 OpenAI 消息标准化,再序列化成按轮次编号的 `DS2API_HISTORY.txt` 风格 transcript,带有 `# DS2API_HISTORY.txt` 标题和 `=== N. ROLE ===` 分段;live prompt 中则只保留一个中性的 user 消息要求模型直接回答最新请求,不再暴露文件名或要求模型读取本地文件。 +- `current_input_file` 默认开启;它用于把“完整上下文”合并进 `DS2API_HISTORY.txt` 上下文文件。当最新 user turn 的纯文本长度达到 `current_input_file.min_chars`(默认 `0`)时,兼容层会上传一个文件名为 `DS2API_HISTORY.txt` 的上下文文件。文件内容会先做 OpenAI 消息标准化,再序列化成按轮次编号的 `DS2API_HISTORY.txt` 风格 transcript,带有 `# DS2API_HISTORY.txt` 标题和 `=== N. ROLE ===` 分段;live prompt 中则会给出一个 continuation 语气的 user 消息,引导模型从 `DS2API_HISTORY.txt` 的最新状态继续推进,并直接回答最新请求,避免把任务拉回起点。 - 如果 `current_input_file.enabled=false`,请求会直接透传,不上传任何拆分上下文文件。 - 旧的 `history_split.enabled` / `history_split.trigger_after_turns` 会被读取进配置对象以保持兼容,但不会触发拆分上传,也不会影响 `current_input_file` 的默认开启。 - 即使触发 `current_input_file` 后 live prompt 被缩短,对客户端回包里的上下文 token 统计,仍会沿用**拆分前的完整 prompt 语义**做计数,而不是按缩短后的占位 prompt 计算;否则会把真实上下文显著算小。 @@ -332,7 +332,7 @@ Prior conversation history and tool progress. ```json { - "prompt": "<|begin▁of▁sentence|><|System|>原 system / developer\n\nYou have access to these tools: ...<|end▁of▁instructions|><|User|>The current request and prior conversation context have already been provided. Answer the latest user request directly.<|Assistant|>", + "prompt": "<|begin▁of▁sentence|><|System|>原 system / developer\n\nYou have access to these tools: ...<|end▁of▁instructions|><|User|>Continue from the latest state in the attached DS2API_HISTORY.txt context. Treat it as the current working state and answer the latest user request directly.<|Assistant|>", "ref_file_ids": [ "file-current-input-ignore", "file-systemprompt", diff --git a/internal/httpapi/openai/chat/chat_history_test.go b/internal/httpapi/openai/chat/chat_history_test.go index 89bc02d..e0c47fc 100644 --- a/internal/httpapi/openai/chat/chat_history_test.go +++ b/internal/httpapi/openai/chat/chat_history_test.go @@ -318,9 +318,9 @@ func TestChatCompletionsCurrentInputFilePersistsNeutralPrompt(t *testing.T) { t.Fatalf("expected uploaded current input file to be persisted in history text") } if len(full.Messages) != 1 { - t.Fatalf("expected neutral prompt to be the only persisted message, got %#v", full.Messages) + t.Fatalf("expected continuation prompt to be the only persisted message, got %#v", full.Messages) } - if !strings.Contains(full.Messages[0].Content, "Answer the latest user request directly.") { - t.Fatalf("expected neutral prompt to be persisted, got %#v", full.Messages[0]) + if !strings.Contains(full.Messages[0].Content, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") { + t.Fatalf("expected continuation prompt to be persisted, got %#v", full.Messages[0]) } } diff --git a/internal/httpapi/openai/chat/vercel_prepare_test.go b/internal/httpapi/openai/chat/vercel_prepare_test.go index 59e62d9..b27be18 100644 --- a/internal/httpapi/openai/chat/vercel_prepare_test.go +++ b/internal/httpapi/openai/chat/vercel_prepare_test.go @@ -130,8 +130,8 @@ func TestHandleVercelStreamPrepareAppliesCurrentInputFile(t *testing.T) { t.Fatalf("expected payload object, got %#v", body["payload"]) } promptText, _ := payload["prompt"].(string) - if !strings.Contains(promptText, "Answer the latest user request directly.") { - t.Fatalf("expected neutral prompt, got %s", promptText) + if !strings.Contains(promptText, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") { + t.Fatalf("expected continuation prompt, got %s", promptText) } if strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { t.Fatalf("expected original turns hidden from prompt, got %s", promptText) diff --git a/internal/httpapi/openai/history/current_input_file.go b/internal/httpapi/openai/history/current_input_file.go index 10d5297..648331c 100644 --- a/internal/httpapi/openai/history/current_input_file.go +++ b/internal/httpapi/openai/history/current_input_file.go @@ -62,7 +62,7 @@ func (s Service) ApplyCurrentInputFile(ctx context.Context, a *auth.RequestAuth, stdReq.RefFileIDs = prependUniqueRefFileID(stdReq.RefFileIDs, fileID) stdReq.FinalPrompt, stdReq.ToolNames = promptcompat.BuildOpenAIPrompt(messages, stdReq.ToolsRaw, "", stdReq.ToolChoice, stdReq.Thinking) // Token accounting must reflect the actual downstream context: - // the uploaded DS2API_HISTORY.txt file content + the neutral live prompt. + // the uploaded DS2API_HISTORY.txt file content + the continuation live prompt. stdReq.PromptTokenText = fileText + "\n" + stdReq.FinalPrompt return stdReq, nil } @@ -87,5 +87,5 @@ func latestUserInputForFile(messages []any) (int, string) { } func currentInputFilePrompt() string { - return "The current request and prior conversation context have already been provided. Answer the latest user request directly." + return "Continue from the latest state in the attached DS2API_HISTORY.txt context. Treat it as the current working state and answer the latest user request directly." } diff --git a/internal/httpapi/openai/history_split_test.go b/internal/httpapi/openai/history_split_test.go index 27a3bf7..d223689 100644 --- a/internal/httpapi/openai/history_split_test.go +++ b/internal/httpapi/openai/history_split_test.go @@ -303,11 +303,11 @@ func TestApplyCurrentInputFileUploadsFirstTurnWithNumberedHistoryTranscript(t *t if strings.Contains(out.FinalPrompt, "first turn content that is long enough") { t.Fatalf("expected current input text to be replaced in live prompt, got %s", out.FinalPrompt) } - if strings.Contains(out.FinalPrompt, "CURRENT_USER_INPUT.txt") || strings.Contains(out.FinalPrompt, "DS2API_HISTORY.txt") || strings.Contains(out.FinalPrompt, "DS2API_HISTORY.txt") || strings.Contains(out.FinalPrompt, "Read that file") { + if strings.Contains(out.FinalPrompt, "CURRENT_USER_INPUT.txt") || strings.Contains(out.FinalPrompt, "Read that file") { t.Fatalf("expected live prompt not to instruct file reads, got %s", out.FinalPrompt) } - if !strings.Contains(out.FinalPrompt, "Answer the latest user request directly.") { - t.Fatalf("expected neutral continuation instruction in live prompt, got %s", out.FinalPrompt) + if !strings.Contains(out.FinalPrompt, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") { + t.Fatalf("expected continuation-oriented prompt in live prompt, got %s", out.FinalPrompt) } if len(out.RefFileIDs) != 1 || out.RefFileIDs[0] != "file-inline-1" { t.Fatalf("expected current input file id in ref_file_ids, got %#v", out.RefFileIDs) @@ -358,8 +358,8 @@ func TestApplyCurrentInputFilePreservesFullContextPromptForTokenCounting(t *test if !strings.Contains(out.PromptTokenText, "# DS2API_HISTORY.txt") || !strings.Contains(out.PromptTokenText, "=== 1. SYSTEM ===") { t.Fatalf("expected prompt token text to include numbered history transcript, got %q", out.PromptTokenText) } - if !strings.Contains(out.PromptTokenText, "Answer the latest user request directly.") { - t.Fatalf("expected prompt token text to also include neutral live prompt, got %q", out.PromptTokenText) + if !strings.Contains(out.PromptTokenText, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") { + t.Fatalf("expected prompt token text to also include continuation prompt, got %q", out.PromptTokenText) } if strings.Contains(out.FinalPrompt, "first user turn") || strings.Contains(out.FinalPrompt, "latest user turn") { t.Fatalf("expected live prompt to hide original turns, got %q", out.FinalPrompt) @@ -406,11 +406,11 @@ func TestApplyCurrentInputFileUploadsFullContextFile(t *testing.T) { t.Fatalf("expected full context file to contain %q, got %q", want, uploadedText) } } - if strings.Contains(out.FinalPrompt, "first user turn") || strings.Contains(out.FinalPrompt, "latest user turn") || strings.Contains(out.FinalPrompt, "CURRENT_USER_INPUT.txt") || strings.Contains(out.FinalPrompt, "DS2API_HISTORY.txt") || strings.Contains(out.FinalPrompt, "DS2API_HISTORY.txt") || strings.Contains(out.FinalPrompt, "Read that file") { - t.Fatalf("expected live prompt to use only a neutral continuation instruction, got %s", out.FinalPrompt) + if strings.Contains(out.FinalPrompt, "first user turn") || strings.Contains(out.FinalPrompt, "latest user turn") || strings.Contains(out.FinalPrompt, "CURRENT_USER_INPUT.txt") || strings.Contains(out.FinalPrompt, "Read that file") { + t.Fatalf("expected live prompt to use only a continuation instruction, got %s", out.FinalPrompt) } - if !strings.Contains(out.FinalPrompt, "Answer the latest user request directly.") { - t.Fatalf("expected neutral continuation instruction in live prompt, got %s", out.FinalPrompt) + if !strings.Contains(out.FinalPrompt, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") { + t.Fatalf("expected continuation-oriented prompt in live prompt, got %s", out.FinalPrompt) } } @@ -496,8 +496,8 @@ func TestChatCompletionsCurrentInputFileUploadsContextAndKeepsNeutralPrompt(t *t t.Fatal("expected completion payload to be captured") } promptText, _ := ds.completionReq["prompt"].(string) - if !strings.Contains(promptText, "Answer the latest user request directly.") { - t.Fatalf("expected neutral completion prompt, got %s", promptText) + if !strings.Contains(promptText, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") { + t.Fatalf("expected continuation-oriented prompt, got %s", promptText) } if strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { t.Fatalf("expected prompt to hide original turns, got %s", promptText) @@ -556,8 +556,8 @@ func TestResponsesCurrentInputFileUploadsContextAndKeepsNeutralPrompt(t *testing t.Fatal("expected completion payload to be captured") } promptText, _ := ds.completionReq["prompt"].(string) - if !strings.Contains(promptText, "Answer the latest user request directly.") { - t.Fatalf("expected neutral completion prompt, got %s", promptText) + if !strings.Contains(promptText, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") { + t.Fatalf("expected continuation-oriented prompt, got %s", promptText) } if strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { t.Fatalf("expected prompt to hide original turns, got %s", promptText) @@ -706,7 +706,7 @@ func TestCurrentInputFileWorksAcrossAutoDeleteModes(t *testing.T) { t.Fatalf("expected completion payload for mode=%s", mode) } promptText, _ := ds.completionReq["prompt"].(string) - if !strings.Contains(promptText, "Answer the latest user request directly.") || strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { + if !strings.Contains(promptText, "Continue from the latest state in the attached DS2API_HISTORY.txt context.") || strings.Contains(promptText, "first user turn") || strings.Contains(promptText, "latest user turn") { t.Fatalf("unexpected prompt for mode=%s: %s", mode, promptText) } }) diff --git a/webui/src/features/chatHistory/ChatHistoryContainer.jsx b/webui/src/features/chatHistory/ChatHistoryContainer.jsx index e0b574f..c0dc98c 100644 --- a/webui/src/features/chatHistory/ChatHistoryContainer.jsx +++ b/webui/src/features/chatHistory/ChatHistoryContainer.jsx @@ -16,7 +16,15 @@ const TOOL_MARKER = '<|Tool|>' const END_INSTRUCTIONS_MARKER = '<|end▁of▁instructions|>' const END_SENTENCE_MARKER = '<|end▁of▁sentence|>' const END_TOOL_RESULTS_MARKER = '<|end▁of▁toolresults|>' -const CURRENT_INPUT_FILE_PROMPT = 'The current request and prior conversation context have already been provided. Answer the latest user request directly.' +const CURRENT_INPUT_FILE_PROMPT = 'Continue from the latest state in the attached DS2API_HISTORY.txt context. Treat it as the current working state and answer the latest user request directly.' +const LEGACY_CURRENT_INPUT_FILE_PROMPTS = new Set([ + 'The current request and prior conversation context have already been provided. Answer the latest user request directly.', +]) + +function isCurrentInputFilePrompt(value) { + const text = String(value || '').trim() + return text === CURRENT_INPUT_FILE_PROMPT || LEGACY_CURRENT_INPUT_FILE_PROMPTS.has(text) +} function formatDateTime(value, lang) { if (!value) return '-' @@ -312,7 +320,7 @@ function buildListModeMessages(item, t) { const placeholderOnly = liveMessages.length === 1 && String(liveMessages[0]?.role || '').trim().toLowerCase() === 'user' - && String(liveMessages[0]?.content || '').trim() === CURRENT_INPUT_FILE_PROMPT + && isCurrentInputFilePrompt(liveMessages[0]?.content) if (placeholderOnly) { return { messages: historyMessages, historyMerged: true }