diff --git a/proxy/translator.go b/proxy/translator.go index 4a4d312..4c5dc62 100644 --- a/proxy/translator.go +++ b/proxy/translator.go @@ -10,6 +10,23 @@ import ( "github.com/google/uuid" ) +// reSysPromptBlock matches the --- SYSTEM PROMPT --- wrapper this proxy injects around system +// prompts before forwarding to Kiro. When Kiro echoes the content back the block can end up +// in subsequent conversation history and must be stripped before the next upstream request. +var reSysPromptBlock = regexp.MustCompile(`(?s)---+\s*SYSTEM\s+PROMPT\s*---+.*?---+\s*END\s+SYSTEM\s+PROMPT\s*---+\n?`) + +// reSystemReminder matches blocks injected by Claude Code into conversation +// context; these are internal metadata and must not be forwarded to Kiro. +var reSystemReminder = regexp.MustCompile(`(?s).*?\n?`) + +// sanitizeText removes known proxy-injected and tool-injected marker blocks from message text +// so they are not forwarded upstream where they can pollute Kiro's context. +func sanitizeText(s string) string { + s = reSysPromptBlock.ReplaceAllString(s, "") + s = reSystemReminder.ReplaceAllString(s, "") + return strings.TrimSpace(s) +} + // 模型映射(有序,长 key 优先匹配,避免 "claude-sonnet-4" 误匹配 "claude-sonnet-4.5") type modelMapping struct { key string @@ -224,6 +241,7 @@ func ClaudeToKiro(req *ClaudeRequest, thinking bool) *KiroPayload { if msg.Role == "user" { content, images, toolResults := extractClaudeUserContent(msg.Content) + content = sanitizeText(content) content = normalizeUserContent(content, len(images) > 0) if isLast { @@ -250,6 +268,7 @@ func ClaudeToKiro(req *ClaudeRequest, thinking bool) *KiroPayload { } } else if msg.Role == "assistant" { content, toolUses := extractClaudeAssistantContent(msg.Content) + content = sanitizeText(content) history = append(history, KiroHistoryMessage{ AssistantResponseMessage: &KiroAssistantResponseMessage{ Content: content, @@ -731,6 +750,7 @@ func OpenAIToKiro(req *OpenAIRequest, thinking bool) *KiroPayload { switch msg.Role { case "user": content, images := extractOpenAIUserContent(msg.Content) + content = sanitizeText(content) content = normalizeUserContent(content, len(images) > 0) // 第一条 user 消息合并 system prompt @@ -754,7 +774,7 @@ func OpenAIToKiro(req *OpenAIRequest, thinking bool) *KiroPayload { } case "assistant": - content := extractOpenAIMessageText(msg.Content) + content := sanitizeText(extractOpenAIMessageText(msg.Content)) var toolUses []KiroToolUse for _, tc := range msg.ToolCalls { diff --git a/proxy/translator_test.go b/proxy/translator_test.go index 984c97f..822c168 100644 --- a/proxy/translator_test.go +++ b/proxy/translator_test.go @@ -314,3 +314,71 @@ func TestParseModelAndThinkingNoSilentDowngrade(t *testing.T) { } } } + +func TestSanitizeTextStripsInjectionBlocks(t *testing.T) { + cases := []struct { + name string + input string + want string + }{ + { + name: "strips system prompt block", + input: "--- SYSTEM PROMPT ---\nYou are Kiro.\n--- END SYSTEM PROMPT ---\n\nHello", + want: "Hello", + }, + { + name: "strips system reminder block", + input: "\nsome metadata\n\n\nActual question", + want: "Actual question", + }, + { + name: "strips both blocks", + input: "--- SYSTEM PROMPT ---\nidentity\n--- END SYSTEM PROMPT ---\nctx\nReal content", + want: "Real content", + }, + { + name: "passes through clean text unchanged", + input: "Just a normal message", + want: "Just a normal message", + }, + { + name: "handles multiple dashes", + input: "---- SYSTEM PROMPT ----\nfoo\n---- END SYSTEM PROMPT ----\nbar", + want: "bar", + }, + } + for _, tc := range cases { + got := sanitizeText(tc.input) + if got != tc.want { + t.Errorf("sanitizeText(%q) [%s] = %q; want %q", tc.input, tc.name, got, tc.want) + } + } +} + +func TestClaudeToKiroSanitizesHistoryContent(t *testing.T) { + injectedAssistant := "--- SYSTEM PROMPT ---\nYou are Kiro.\n--- END SYSTEM PROMPT ---\n\nHere is my answer." + req := &ClaudeRequest{ + Model: "claude-sonnet-4.5", + Messages: []ClaudeMessage{ + {Role: "user", Content: "hello"}, + {Role: "assistant", Content: injectedAssistant}, + {Role: "user", Content: "continue"}, + }, + } + + payload := ClaudeToKiro(req, false) + + if len(payload.ConversationState.History) < 2 { + t.Fatalf("expected at least 2 history entries, got %d", len(payload.ConversationState.History)) + } + assistantEntry := payload.ConversationState.History[1].AssistantResponseMessage + if assistantEntry == nil { + t.Fatalf("expected second history entry to be assistant message") + } + if strings.Contains(assistantEntry.Content, "SYSTEM PROMPT") { + t.Errorf("assistant history content still contains injection block: %q", assistantEntry.Content) + } + if !strings.Contains(assistantEntry.Content, "Here is my answer") { + t.Errorf("assistant history content missing expected text: %q", assistantEntry.Content) + } +}