Files
ds2api/internal/promptcompat/prompt_build_test.go
waiwai f33789399e fix(toolcall): correct DSML closing tag slash position
The closing tag format was <|/DSML|tag> but must be </|DSML|tag>.
The scanner's closing-tag detection checks text[1] == '/', so the
slash must come immediately after '<', before the first full-width
pipe (U+FF5C). Tags like <|/DSML|tool_calls> would not set
closing=true and would not match any tool markup name.

Files fixed:
- internal/toolcall/tool_prompt.go: all closing tags
- internal/promptcompat/prompt_build_test.go: 1 test expectation
2026-05-09 16:42:22 +08:00

186 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package promptcompat
import (
"strings"
"testing"
)
func TestBuildOpenAIFinalPrompt_HandlerPathIncludesToolRoundtripSemantics(t *testing.T) {
messages := []any{
map[string]any{"role": "user", "content": "查北京天气"},
map[string]any{
"role": "assistant",
"tool_calls": []any{
map[string]any{
"id": "call_1",
"function": map[string]any{
"name": "get_weather",
"arguments": "{\"city\":\"beijing\"}",
},
},
},
},
map[string]any{
"role": "tool",
"tool_call_id": "call_1",
"name": "get_weather",
"content": map[string]any{"temp": 18, "condition": "sunny"},
},
}
tools := []any{
map[string]any{
"type": "function",
"function": map[string]any{
"name": "get_weather",
"description": "Get weather",
"parameters": map[string]any{
"type": "object",
},
},
},
}
finalPrompt, toolNames := buildOpenAIFinalPrompt(messages, tools, "", false)
if len(toolNames) != 1 || toolNames[0] != "get_weather" {
t.Fatalf("unexpected tool names: %#v", toolNames)
}
if !strings.Contains(finalPrompt, `"condition":"sunny"`) {
t.Fatalf("handler finalPrompt should preserve tool output content: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "<|DSML|tool_calls>") {
t.Fatalf("handler finalPrompt should preserve assistant tool history: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, `<|DSML|invoke name="get_weather">`) {
t.Fatalf("handler finalPrompt should include tool name history: %q", finalPrompt)
}
}
func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t *testing.T) {
messages := []any{
map[string]any{"role": "system", "content": "You are helpful"},
map[string]any{"role": "user", "content": "请调用工具"},
}
tools := []any{
map[string]any{
"type": "function",
"function": map[string]any{
"name": "search",
"description": "search docs",
"parameters": map[string]any{
"type": "object",
},
},
},
}
finalPrompt, _ := buildOpenAIFinalPrompt(messages, tools, "", false)
if !strings.Contains(finalPrompt, "Remember: The ONLY valid way to use tools is the <DSMLtool_calls>...</DSMLtool_calls> block at the end of your response.") {
t.Fatalf("vercel prepare finalPrompt missing final tool-call anchor instruction: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "TOOL CALL FORMAT") {
t.Fatalf("vercel prepare finalPrompt missing xml format instruction: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "Do NOT wrap XML in markdown fences") {
t.Fatalf("vercel prepare finalPrompt missing no-fence xml instruction: %q", finalPrompt)
}
if strings.Contains(finalPrompt, "```json") {
t.Fatalf("vercel prepare finalPrompt should not require fenced tool calls: %q", finalPrompt)
}
}
func TestBuildOpenAIFinalPromptPrependsOutputIntegrityGuard(t *testing.T) {
messages := []any{
map[string]any{"role": "system", "content": "You are helpful"},
map[string]any{"role": "user", "content": "请调用工具"},
}
tools := []any{
map[string]any{
"type": "function",
"function": map[string]any{
"name": "search",
"description": "search docs",
"parameters": map[string]any{
"type": "object",
},
},
},
}
finalPrompt, _ := buildOpenAIFinalPrompt(messages, tools, "", false)
guardIdx := strings.Index(finalPrompt, "Output integrity guard")
toolIdx := strings.Index(finalPrompt, "TOOL CALL FORMAT")
if guardIdx < 0 {
t.Fatalf("expected output integrity guard in final prompt, got: %q", finalPrompt)
}
if toolIdx < 0 {
t.Fatalf("expected tool instructions in final prompt, got: %q", finalPrompt)
}
if guardIdx > toolIdx {
t.Fatalf("expected output integrity guard to precede tool instructions, got: %q", finalPrompt)
}
}
func TestBuildOpenAIFinalPromptReadLikeToolIncludesCacheGuard(t *testing.T) {
messages := []any{
map[string]any{"role": "user", "content": "请读取文件"},
}
tools := []any{
map[string]any{
"type": "function",
"function": map[string]any{
"name": "read_file",
"description": "Read a file",
"parameters": map[string]any{
"type": "object",
},
},
},
}
finalPrompt, _ := buildOpenAIFinalPrompt(messages, tools, "", false)
if !strings.Contains(finalPrompt, "Read-tool cache guard") {
t.Fatalf("read-like tool prompt missing cache guard: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "provides no file body") {
t.Fatalf("read-like tool prompt missing no-body handling: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "Do not repeatedly call the same read request") {
t.Fatalf("read-like tool prompt missing loop guard: %q", finalPrompt)
}
}
func TestBuildOpenAIFinalPromptNonReadToolOmitsCacheGuard(t *testing.T) {
messages := []any{
map[string]any{"role": "user", "content": "搜索一下"},
}
tools := []any{
map[string]any{
"type": "function",
"function": map[string]any{
"name": "search",
"description": "Search docs",
"parameters": map[string]any{
"type": "object",
},
},
},
}
finalPrompt, _ := buildOpenAIFinalPrompt(messages, tools, "", false)
if strings.Contains(finalPrompt, "Read-tool cache guard") {
t.Fatalf("non-read tool prompt should not include read cache guard: %q", finalPrompt)
}
}
func TestBuildOpenAIFinalPromptWithThinkingKeepsPromptUnchanged(t *testing.T) {
messages := []any{
map[string]any{"role": "user", "content": "继续回答上一个问题"},
}
finalPromptThinking, _ := buildOpenAIFinalPrompt(messages, nil, "", true)
finalPromptPlain, _ := buildOpenAIFinalPrompt(messages, nil, "", false)
if finalPromptThinking != finalPromptPlain {
t.Fatalf("expected thinking flag not to prepend continuation contract, thinking=%q plain=%q", finalPromptThinking, finalPromptPlain)
}
}