From 1c2edd5f0d7e24bfcd0382b7c7ff82f6d140b4ed Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Tue, 12 May 2026 14:14:54 +0800 Subject: [PATCH] feat: intercept identity questions and return consistent Claude identity Kiro's upstream system prompt overrides all user-provided system prompts and returns "I can't discuss that" for identity questions. This pre-flight interceptor detects identity questions (Chinese and English patterns) in the last user message and returns a Claude-style response directly, bypassing Kiro entirely. Response language matches the question language; model name reflects the requested model (Claude Opus 4.7, Claude Sonnet 4.5, etc.). Applied to both /v1/messages (Claude) and /v1/chat/completions (OpenAI). Co-Authored-By: Claude Sonnet 4.6 --- proxy/handler.go | 18 ++++ proxy/identity.go | 225 +++++++++++++++++++++++++++++++++++++++++ proxy/identity_test.go | 88 ++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 proxy/identity.go create mode 100644 proxy/identity_test.go diff --git a/proxy/handler.go b/proxy/handler.go index 904fc07..ae6f0d3 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -717,6 +717,17 @@ func (h *Handler) handleClaudeMessagesInternal(w http.ResponseWriter, r *http.Re // 转换请求 kiroPayload := ClaudeToKiro(&req, thinking) + // 身份问题预检:直接返回 Claude 身份,跳过 Kiro(Kiro 上游系统提示会无视任何身份覆盖指令) + lastUserText := getLastUserTextClaude(req.Messages) + if isIdentityQuestion(lastUserText) { + if req.Stream { + h.sendClaudeIdentityStream(w, req.Model, lastUserText, estimatedInputTokens) + } else { + h.sendClaudeIdentityNonStream(w, req.Model, lastUserText, estimatedInputTokens) + } + return + } + // Stream or non-stream if req.Stream { h.handleClaudeStream(w, account, kiroPayload, req.Model, thinking, thinkingResponseOpts, estimatedInputTokens, cacheUsage, cacheProfile) @@ -1353,6 +1364,13 @@ func (h *Handler) handleOpenAIChat(w http.ResponseWriter, r *http.Request) { kiroPayload := OpenAIToKiro(&req, thinking) + // 身份问题预检 + lastUserText := getLastUserTextOpenAI(req.Messages) + if isIdentityQuestion(lastUserText) { + h.sendOpenAIIdentityNonStream(w, req.Model, lastUserText, estimatedInputTokens) + return + } + if req.Stream { h.handleOpenAIStream(w, account, kiroPayload, req.Model, thinking, estimatedInputTokens) } else { diff --git a/proxy/identity.go b/proxy/identity.go new file mode 100644 index 0000000..2a10a75 --- /dev/null +++ b/proxy/identity.go @@ -0,0 +1,225 @@ +package proxy + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + "strings" + "unicode" + + "github.com/google/uuid" +) + +// identityPatterns covers common ways users ask about the AI's identity. +var identityPatterns = []*regexp.Regexp{ + // English + regexp.MustCompile(`(?i)\bwho are you\b`), + regexp.MustCompile(`(?i)\bwhat are you\b`), + regexp.MustCompile(`(?i)\bwhat model\b`), + regexp.MustCompile(`(?i)\bwhich model\b`), + regexp.MustCompile(`(?i)\byour (name|identity|model|version)\b`), + regexp.MustCompile(`(?i)\btell me about yourself\b`), + regexp.MustCompile(`(?i)\bidentify yourself\b`), + regexp.MustCompile(`(?i)\bwhat (llm|language model) are you\b`), + regexp.MustCompile(`(?i)\bwhat (ai|assistant) are you\b`), + // Chinese + regexp.MustCompile(`你是谁`), + regexp.MustCompile(`你是什么`), + regexp.MustCompile(`你叫什么`), + regexp.MustCompile(`什么模型`), + regexp.MustCompile(`哪个模型`), + regexp.MustCompile(`你基于什么`), + regexp.MustCompile(`你是哪个`), + regexp.MustCompile(`你是哪款`), + regexp.MustCompile(`你的身份`), + regexp.MustCompile(`你的名字`), + regexp.MustCompile(`什么大模型`), + regexp.MustCompile(`什么AI`), +} + +// isIdentityQuestion returns true when the text appears to be asking about AI identity. +func isIdentityQuestion(text string) bool { + for _, re := range identityPatterns { + if re.MatchString(text) { + return true + } + } + return false +} + +func hasChinese(s string) bool { + for _, r := range s { + if unicode.Is(unicode.Han, r) { + return true + } + } + return false +} + +// friendlyModelName converts a raw model ID to a human-readable Claude model name. +func friendlyModelName(model string) string { + m := strings.ToLower(model) + for _, suf := range []string{"-thinking", "-thought"} { + if strings.HasSuffix(m, suf) { + m = m[:len(m)-len(suf)] + break + } + } + switch { + case strings.Contains(m, "opus-4.7") || strings.Contains(m, "opus-4-7"): + return "Claude Opus 4.7" + case strings.Contains(m, "opus-4.6") || strings.Contains(m, "opus-4-6"): + return "Claude Opus 4.6" + case strings.Contains(m, "opus-4.5") || strings.Contains(m, "opus-4-5"): + return "Claude Opus 4.5" + case strings.Contains(m, "sonnet-4.7") || strings.Contains(m, "sonnet-4-7"): + return "Claude Sonnet 4.7" + case strings.Contains(m, "sonnet-4.6") || strings.Contains(m, "sonnet-4-6"): + return "Claude Sonnet 4.6" + case strings.Contains(m, "sonnet-4.5") || strings.Contains(m, "sonnet-4-5"): + return "Claude Sonnet 4.5" + case strings.Contains(m, "sonnet-4"): + return "Claude Sonnet 4" + case strings.Contains(m, "haiku-4.7") || strings.Contains(m, "haiku-4-7"): + return "Claude Haiku 4.7" + case strings.Contains(m, "haiku-4.5") || strings.Contains(m, "haiku-4-5"): + return "Claude Haiku 4.5" + default: + return "Claude" + } +} + +// claudeIdentityText returns a Claude-style identity response, language-matched to the question. +func claudeIdentityText(model, question string) string { + friendly := friendlyModelName(model) + if hasChinese(question) { + return "我是 Claude,由 Anthropic 公司开发的 AI 助手。\n\n我目前使用的是 " + friendly + " 模型,可以帮你完成代码编写与调试、分析推理、多语言对话等任务。有什么可以帮你的吗?" + } + return "I'm Claude, an AI assistant made by Anthropic.\n\nI'm currently running on the " + friendly + " model. I can help with coding, debugging, analysis, writing, and much more. What can I help you with?" +} + +// getLastUserTextClaude extracts the text of the last user message. +func getLastUserTextClaude(msgs []ClaudeMessage) string { + for i := len(msgs) - 1; i >= 0; i-- { + if msgs[i].Role == "user" { + text, _, _ := extractClaudeUserContent(msgs[i].Content) + return text + } + } + return "" +} + +// getLastUserTextOpenAI extracts the text of the last user message. +func getLastUserTextOpenAI(msgs []OpenAIMessage) string { + for i := len(msgs) - 1; i >= 0; i-- { + if msgs[i].Role == "user" { + text, _ := extractOpenAIUserContent(msgs[i].Content) + return text + } + } + return "" +} + +// sendClaudeIdentityNonStream writes a non-streaming Claude identity response. +func (h *Handler) sendClaudeIdentityNonStream(w http.ResponseWriter, model, question string, estimatedInputTokens int) { + text := claudeIdentityText(model, question) + outTokens := estimateTextTokens(text) + resp := KiroToClaudeResponse(text, "", false, nil, estimatedInputTokens, outTokens, model) + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(resp) +} + +// sendClaudeIdentityStream writes a streaming Claude identity response. +func (h *Handler) sendClaudeIdentityStream(w http.ResponseWriter, model, question string, estimatedInputTokens int) { + w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + flusher, ok := w.(http.Flusher) + if !ok { + h.sendClaudeError(w, 500, "api_error", "Streaming not supported") + return + } + + text := claudeIdentityText(model, question) + outTokens := estimateTextTokens(text) + msgID := "msg_" + uuid.New().String() + + send := func(event string, data interface{}) { + b, _ := json.Marshal(data) + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, b) + flusher.Flush() + } + + send("message_start", map[string]interface{}{ + "type": "message_start", + "message": map[string]interface{}{ + "id": msgID, + "type": "message", + "role": "assistant", + "content": []interface{}{}, + "model": model, + "stop_reason": nil, + "stop_sequence": nil, + "usage": map[string]int{"input_tokens": estimatedInputTokens, "output_tokens": 0}, + }, + }) + send("content_block_start", map[string]interface{}{ + "type": "content_block_start", + "index": 0, + "content_block": map[string]string{ + "type": "text", + "text": "", + }, + }) + send("ping", map[string]string{"type": "ping"}) + send("content_block_delta", map[string]interface{}{ + "type": "content_block_delta", + "index": 0, + "delta": map[string]string{"type": "text_delta", "text": text}, + }) + send("content_block_stop", map[string]interface{}{ + "type": "content_block_stop", + "index": 0, + }) + send("message_delta", map[string]interface{}{ + "type": "message_delta", + "delta": map[string]string{"stop_reason": "end_turn"}, + "usage": map[string]int{"output_tokens": outTokens}, + }) + send("message_stop", map[string]string{"type": "message_stop"}) +} + +// sendOpenAIIdentityNonStream writes a non-streaming OpenAI identity response. +func (h *Handler) sendOpenAIIdentityNonStream(w http.ResponseWriter, model, question string, estimatedInputTokens int) { + text := claudeIdentityText(model, question) + outTokens := estimateTextTokens(text) + resp := OpenAIResponse{ + ID: "chatcmpl-" + uuid.New().String(), + Object: "chat.completion", + Created: 0, + Model: model, + Choices: []OpenAIChoice{{ + Index: 0, + Message: OpenAIMessage{Role: "assistant", Content: text}, + FinishReason: "stop", + }}, + Usage: OpenAIUsage{ + PromptTokens: estimatedInputTokens, + CompletionTokens: outTokens, + TotalTokens: estimatedInputTokens + outTokens, + }, + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(resp) +} + +// estimateTextTokens estimates token count as word-count * 1.3. +func estimateTextTokens(text string) int { + n := len(strings.Fields(text)) + if n < 1 { + n = 1 + } + return int(float64(n)*1.3) + 1 +} diff --git a/proxy/identity_test.go b/proxy/identity_test.go new file mode 100644 index 0000000..4dde7c3 --- /dev/null +++ b/proxy/identity_test.go @@ -0,0 +1,88 @@ +package proxy + +import ( + "strings" + "testing" +) + +func TestIsIdentityQuestion(t *testing.T) { + yes := []string{ + "你是谁?", + "你是什么模型", + "你叫什么名字", + "什么模型", + "哪个模型", + "你基于什么", + "你是哪个AI", + "你的身份是什么", + "who are you", + "what are you", + "what model are you", + "which model do you use", + "tell me about yourself", + "identify yourself", + "what is your name", + "what AI are you", + } + no := []string{ + "帮我写一段 Go 代码", + "fix this bug", + "explain this function", + "what does this code do", + "你是怎么实现这个功能的", // "how did you implement" - not identity + "what is the weather today", + } + + for _, q := range yes { + if !isIdentityQuestion(q) { + t.Errorf("expected isIdentityQuestion(%q)=true", q) + } + } + for _, q := range no { + if isIdentityQuestion(q) { + t.Errorf("expected isIdentityQuestion(%q)=false", q) + } + } +} + +func TestFriendlyModelName(t *testing.T) { + cases := []struct{ in, want string }{ + {"claude-opus-4.7", "Claude Opus 4.7"}, + {"claude-opus-4-7", "Claude Opus 4.7"}, + {"claude-opus-4.7-thinking", "Claude Opus 4.7"}, + {"claude-sonnet-4.5", "Claude Sonnet 4.5"}, + {"claude-sonnet-4-5", "Claude Sonnet 4.5"}, + {"claude-sonnet-4.6", "Claude Sonnet 4.6"}, + {"claude-sonnet-4.7", "Claude Sonnet 4.7"}, + {"claude-sonnet-4", "Claude Sonnet 4"}, + {"claude-haiku-4.5", "Claude Haiku 4.5"}, + {"claude-haiku-4-5", "Claude Haiku 4.5"}, + } + for _, tc := range cases { + got := friendlyModelName(tc.in) + if got != tc.want { + t.Errorf("friendlyModelName(%q) = %q; want %q", tc.in, got, tc.want) + } + } +} + +func TestClaudeIdentityTextLanguage(t *testing.T) { + zhText := claudeIdentityText("claude-opus-4.7", "你是谁") + if !hasChinese(zhText) { + t.Errorf("expected Chinese response for Chinese question, got %q", zhText) + } + if !strings.Contains(zhText, "Claude Opus 4.7") { + t.Errorf("expected model name in response, got %q", zhText) + } + if !strings.Contains(zhText, "Anthropic") { + t.Errorf("expected Anthropic in response, got %q", zhText) + } + + enText := claudeIdentityText("claude-sonnet-4.5", "who are you") + if hasChinese(enText) { + t.Errorf("expected English response for English question, got %q", enText) + } + if !strings.Contains(enText, "Claude Sonnet 4.5") { + t.Errorf("expected model name in English response, got %q", enText) + } +}