feat: intercept identity questions and return consistent Claude identity
Some checks failed
Build Docker Image / build (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 14:14:54 +08:00
parent a6e11c6d22
commit 1c2edd5f0d
3 changed files with 331 additions and 0 deletions

View File

@@ -717,6 +717,17 @@ func (h *Handler) handleClaudeMessagesInternal(w http.ResponseWriter, r *http.Re
// 转换请求
kiroPayload := ClaudeToKiro(&req, thinking)
// 身份问题预检:直接返回 Claude 身份,跳过 KiroKiro 上游系统提示会无视任何身份覆盖指令)
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 {

225
proxy/identity.go Normal file
View File

@@ -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
}

88
proxy/identity_test.go Normal file
View File

@@ -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)
}
}