From c729ee425f41e5c1bbfc92bf458a3c709570abc2 Mon Sep 17 00:00:00 2001 From: QTom Date: Fri, 27 Mar 2026 14:44:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20=E4=BF=AE=E5=A4=8D=20OpenAI?= =?UTF-8?q?=E2=86=92Anthropic=20=E8=BD=AC=E6=8D=A2=E8=B7=AF=E5=BE=84=20sys?= =?UTF-8?q?tem=20prompt=20=E8=A2=AB=E9=9D=99=E9=BB=98=E4=B8=A2=E5=BC=83?= =?UTF-8?q?=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit injectClaudeCodePrompt 和 systemIncludesClaudeCodePrompt 的 type switch 无法匹配 json.RawMessage 类型(Go typed nil 陷阱),导致 ForwardAsResponses 和 ForwardAsChatCompletions 路径中用户 system prompt 被替换为仅 Claude Code banner。新增 normalizeSystemParam 将 json.RawMessage 转为标准 Go 类型。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../internal/service/gateway_prompt_test.go | 44 +++++++++++++++++++ backend/internal/service/gateway_service.go | 20 +++++++++ 2 files changed, 64 insertions(+) diff --git a/backend/internal/service/gateway_prompt_test.go b/backend/internal/service/gateway_prompt_test.go index 52c75d1d..356536b0 100644 --- a/backend/internal/service/gateway_prompt_test.go +++ b/backend/internal/service/gateway_prompt_test.go @@ -124,6 +124,27 @@ func TestSystemIncludesClaudeCodePrompt(t *testing.T) { }, want: false, }, + // json.RawMessage cases (conversion path: ForwardAsResponses / ForwardAsChatCompletions) + { + name: "json.RawMessage string with Claude Code prompt", + system: json.RawMessage(`"` + claudeCodeSystemPrompt + `"`), + want: true, + }, + { + name: "json.RawMessage string without Claude Code prompt", + system: json.RawMessage(`"You are a helpful assistant"`), + want: false, + }, + { + name: "json.RawMessage nil (empty)", + system: json.RawMessage(nil), + want: false, + }, + { + name: "json.RawMessage empty string", + system: json.RawMessage(`""`), + want: false, + }, } for _, tt := range tests { @@ -202,6 +223,29 @@ func TestInjectClaudeCodePrompt(t *testing.T) { wantSystemLen: 1, wantFirstText: claudeCodeSystemPrompt, }, + // json.RawMessage cases (conversion path: ForwardAsResponses / ForwardAsChatCompletions) + { + name: "json.RawMessage string system", + body: `{"model":"claude-3","system":"Custom prompt"}`, + system: json.RawMessage(`"Custom prompt"`), + wantSystemLen: 2, + wantFirstText: claudeCodeSystemPrompt, + wantSecondText: claudePrefix + "\n\nCustom prompt", + }, + { + name: "json.RawMessage nil system", + body: `{"model":"claude-3"}`, + system: json.RawMessage(nil), + wantSystemLen: 1, + wantFirstText: claudeCodeSystemPrompt, + }, + { + name: "json.RawMessage Claude Code prompt (should not duplicate)", + body: `{"model":"claude-3","system":"` + claudeCodeSystemPrompt + `"}`, + system: json.RawMessage(`"` + claudeCodeSystemPrompt + `"`), + wantSystemLen: 1, + wantFirstText: claudeCodeSystemPrompt, + }, } for _, tt := range tests { diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index cb90343b..5b7a97b0 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -3749,9 +3749,28 @@ func isClaudeCodeRequest(ctx context.Context, c *gin.Context, parsed *ParsedRequ return isClaudeCodeClient(c.GetHeader("User-Agent"), parsed.MetadataUserID) } +// normalizeSystemParam 将 json.RawMessage 类型的 system 参数转为标准 Go 类型(string / []any / nil), +// 避免 type switch 中 json.RawMessage(底层 []byte)无法匹配 case string / case []any / case nil 的问题。 +// 这是 Go 的 typed nil 陷阱:(json.RawMessage, nil) ≠ (nil, nil)。 +func normalizeSystemParam(system any) any { + raw, ok := system.(json.RawMessage) + if !ok { + return system + } + if len(raw) == 0 { + return nil + } + var parsed any + if err := json.Unmarshal(raw, &parsed); err != nil { + return nil + } + return parsed +} + // systemIncludesClaudeCodePrompt 检查 system 中是否已包含 Claude Code 提示词 // 使用前缀匹配支持多种变体(标准版、Agent SDK 版等) func systemIncludesClaudeCodePrompt(system any) bool { + system = normalizeSystemParam(system) switch v := system.(type) { case string: return hasClaudeCodePrefix(v) @@ -3780,6 +3799,7 @@ func hasClaudeCodePrefix(text string) bool { // injectClaudeCodePrompt 在 system 开头注入 Claude Code 提示词 // 处理 null、字符串、数组三种格式 func injectClaudeCodePrompt(body []byte, system any) []byte { + system = normalizeSystemParam(system) claudeCodeBlock, err := marshalAnthropicSystemTextBlock(claudeCodeSystemPrompt, true) if err != nil { logger.LegacyPrintf("service.gateway", "Warning: failed to build Claude Code prompt block: %v", err)