diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go
index 0beb15d3a1..9368abc76d 100644
--- a/backend/internal/handler/admin/setting_handler.go
+++ b/backend/internal/handler/admin/setting_handler.go
@@ -252,6 +252,9 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EnableFingerprintUnification: settings.EnableFingerprintUnification,
EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
EnableCCHSigning: settings.EnableCCHSigning,
+ EnableClaudeOAuthSystemPromptInjection: settings.EnableClaudeOAuthSystemPromptInjection,
+ ClaudeOAuthSystemPrompt: settings.ClaudeOAuthSystemPrompt,
+ ClaudeOAuthSystemPromptBlocks: settings.ClaudeOAuthSystemPromptBlocks,
EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
@@ -580,14 +583,17 @@ type UpdateSettingsRequest struct {
BackendModeEnabled bool `json:"backend_mode_enabled"`
// Gateway forwarding behavior
- EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
- EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
- EnableCCHSigning *bool `json:"enable_cch_signing"`
- EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
- RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
- AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
- OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
- OpenAIAllowClaudeCodeCodexPlugin *bool `json:"openai_allow_claude_code_codex_plugin"`
+ EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
+ EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
+ EnableCCHSigning *bool `json:"enable_cch_signing"`
+ EnableClaudeOAuthSystemPromptInjection *bool `json:"enable_claude_oauth_system_prompt_injection"`
+ ClaudeOAuthSystemPrompt *string `json:"claude_oauth_system_prompt"`
+ ClaudeOAuthSystemPromptBlocks *string `json:"claude_oauth_system_prompt_blocks"`
+ EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
+ RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
+ AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
+ OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
+ OpenAIAllowClaudeCodeCodexPlugin *bool `json:"openai_allow_claude_code_codex_plugin"`
// Payment visible method routing
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
@@ -1643,6 +1649,24 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.EnableCCHSigning
}(),
+ EnableClaudeOAuthSystemPromptInjection: func() bool {
+ if req.EnableClaudeOAuthSystemPromptInjection != nil {
+ return *req.EnableClaudeOAuthSystemPromptInjection
+ }
+ return previousSettings.EnableClaudeOAuthSystemPromptInjection
+ }(),
+ ClaudeOAuthSystemPrompt: func() string {
+ if req.ClaudeOAuthSystemPrompt != nil {
+ return *req.ClaudeOAuthSystemPrompt
+ }
+ return previousSettings.ClaudeOAuthSystemPrompt
+ }(),
+ ClaudeOAuthSystemPromptBlocks: func() string {
+ if req.ClaudeOAuthSystemPromptBlocks != nil {
+ return *req.ClaudeOAuthSystemPromptBlocks
+ }
+ return previousSettings.ClaudeOAuthSystemPromptBlocks
+ }(),
EnableAnthropicCacheTTL1hInjection: func() bool {
if req.EnableAnthropicCacheTTL1hInjection != nil {
return *req.EnableAnthropicCacheTTL1hInjection
@@ -2045,6 +2069,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification,
EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
EnableCCHSigning: updatedSettings.EnableCCHSigning,
+ EnableClaudeOAuthSystemPromptInjection: updatedSettings.EnableClaudeOAuthSystemPromptInjection,
+ ClaudeOAuthSystemPrompt: updatedSettings.ClaudeOAuthSystemPrompt,
+ ClaudeOAuthSystemPromptBlocks: updatedSettings.ClaudeOAuthSystemPromptBlocks,
EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
@@ -2508,6 +2535,15 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.EnableCCHSigning != after.EnableCCHSigning {
changed = append(changed, "enable_cch_signing")
}
+ if before.EnableClaudeOAuthSystemPromptInjection != after.EnableClaudeOAuthSystemPromptInjection {
+ changed = append(changed, "enable_claude_oauth_system_prompt_injection")
+ }
+ if before.ClaudeOAuthSystemPrompt != after.ClaudeOAuthSystemPrompt {
+ changed = append(changed, "claude_oauth_system_prompt")
+ }
+ if before.ClaudeOAuthSystemPromptBlocks != after.ClaudeOAuthSystemPromptBlocks {
+ changed = append(changed, "claude_oauth_system_prompt_blocks")
+ }
if before.EnableAnthropicCacheTTL1hInjection != after.EnableAnthropicCacheTTL1hInjection {
changed = append(changed, "enable_anthropic_cache_ttl_1h_injection")
}
diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go
index da89ac237c..593a745209 100644
--- a/backend/internal/handler/dto/settings.go
+++ b/backend/internal/handler/dto/settings.go
@@ -178,14 +178,17 @@ type SystemSettings struct {
BackendModeEnabled bool `json:"backend_mode_enabled"`
// Gateway forwarding behavior
- EnableFingerprintUnification bool `json:"enable_fingerprint_unification"`
- EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
- EnableCCHSigning bool `json:"enable_cch_signing"`
- EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"`
- RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
- AntigravityUserAgentVersion string `json:"antigravity_user_agent_version"`
- OpenAICodexUserAgent string `json:"openai_codex_user_agent"`
- OpenAIAllowClaudeCodeCodexPlugin bool `json:"openai_allow_claude_code_codex_plugin"`
+ EnableFingerprintUnification bool `json:"enable_fingerprint_unification"`
+ EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
+ EnableCCHSigning bool `json:"enable_cch_signing"`
+ EnableClaudeOAuthSystemPromptInjection bool `json:"enable_claude_oauth_system_prompt_injection"`
+ ClaudeOAuthSystemPrompt string `json:"claude_oauth_system_prompt"`
+ ClaudeOAuthSystemPromptBlocks string `json:"claude_oauth_system_prompt_blocks"`
+ EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"`
+ RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
+ AntigravityUserAgentVersion string `json:"antigravity_user_agent_version"`
+ OpenAICodexUserAgent string `json:"openai_codex_user_agent"`
+ OpenAIAllowClaudeCodeCodexPlugin bool `json:"openai_allow_claude_code_codex_plugin"`
// Web Search Emulation
WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"`
diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go
index fb9b616adc..8a103c509b 100644
--- a/backend/internal/server/api_contract_test.go
+++ b/backend/internal/server/api_contract_test.go
@@ -835,6 +835,9 @@ func TestAPIContracts(t *testing.T) {
"allow_ungrouped_key_scheduling": false,
"backend_mode_enabled": false,
"enable_cch_signing": false,
+ "enable_claude_oauth_system_prompt_injection": true,
+ "claude_oauth_system_prompt": "",
+ "claude_oauth_system_prompt_blocks": "",
"enable_anthropic_cache_ttl_1h_injection": false,
"rewrite_message_cache_control": false,
"antigravity_user_agent_version": "",
@@ -1075,6 +1078,9 @@ func TestAPIContracts(t *testing.T) {
"enable_fingerprint_unification": true,
"enable_metadata_passthrough": false,
"enable_cch_signing": false,
+ "enable_claude_oauth_system_prompt_injection": true,
+ "claude_oauth_system_prompt": "",
+ "claude_oauth_system_prompt_blocks": "",
"enable_anthropic_cache_ttl_1h_injection": false,
"rewrite_message_cache_control": false,
"antigravity_user_agent_version": "",
diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go
index 11245d0016..870b1ffffb 100644
--- a/backend/internal/service/domain_constants.go
+++ b/backend/internal/service/domain_constants.go
@@ -421,6 +421,12 @@ const (
SettingKeyEnableMetadataPassthrough = "enable_metadata_passthrough"
// SettingKeyEnableCCHSigning 是否对 billing header 中的 cch 进行 xxHash64 签名(默认 false)
SettingKeyEnableCCHSigning = "enable_cch_signing"
+ // SettingKeyEnableClaudeOAuthSystemPromptInjection 是否对 Claude OAuth mimic 路径注入 Claude Code system blocks(默认 true)
+ SettingKeyEnableClaudeOAuthSystemPromptInjection = "enable_claude_oauth_system_prompt_injection"
+ // SettingKeyClaudeOAuthSystemPrompt Claude OAuth mimic 路径注入的通用扩展 system prompt(空值使用内置默认)
+ SettingKeyClaudeOAuthSystemPrompt = "claude_oauth_system_prompt"
+ // SettingKeyClaudeOAuthSystemPromptBlocks Claude OAuth mimic 路径注入的 system blocks JSON 配置(空值使用内置默认)
+ SettingKeyClaudeOAuthSystemPromptBlocks = "claude_oauth_system_prompt_blocks"
// SettingKeyEnableAnthropicCacheTTL1hInjection 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
SettingKeyEnableAnthropicCacheTTL1hInjection = "enable_anthropic_cache_ttl_1h_injection"
// SettingKeyRewriteMessageCacheControl 是否改写 messages[*].content[*].cache_control(默认 false)
diff --git a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go
index c0bc0ef15f..30fc976ef3 100644
--- a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go
+++ b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go
@@ -839,6 +839,70 @@ func TestGatewayService_AnthropicOAuth_ForwardPreservesBillingHeaderSystemBlock(
}
}
+func TestGatewayService_AnthropicOAuth_SystemPromptInjectionCanBeDisabled(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ resetGatewayForwardingSettingsCacheForTest(t)
+
+ rec := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(rec)
+ c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
+
+ body := []byte(`{"model":"claude-3-5-sonnet-latest","system":"Original system prompt","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`)
+ parsed, err := ParseGatewayRequest(NewRequestBodyRef(body), PlatformAnthropic)
+ require.NoError(t, err)
+
+ upstream := &anthropicHTTPUpstreamRecorder{
+ resp: &http.Response{
+ StatusCode: http.StatusOK,
+ Header: http.Header{
+ "Content-Type": []string{"application/json"},
+ "x-request-id": []string{"rid-oauth-no-system-injection"},
+ },
+ Body: io.NopCloser(strings.NewReader(`{"id":"msg_1","type":"message","role":"assistant","model":"claude-3-5-sonnet-20241022","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":12,"output_tokens":7}}`)),
+ },
+ }
+
+ cfg := &config.Config{
+ Gateway: config.GatewayConfig{
+ MaxLineSize: defaultMaxLineSize,
+ },
+ }
+ settingService := NewSettingService(&gatewayTTLSettingRepo{data: map[string]string{
+ SettingKeyEnableClaudeOAuthSystemPromptInjection: "false",
+ }}, cfg)
+ svc := &GatewayService{
+ cfg: cfg,
+ responseHeaderFilter: compileResponseHeaderFilter(cfg),
+ httpUpstream: upstream,
+ rateLimitService: &RateLimitService{},
+ deferredService: &DeferredService{},
+ settingService: settingService,
+ }
+
+ account := &Account{
+ ID: 302,
+ Name: "anthropic-oauth-no-system-injection",
+ Platform: PlatformAnthropic,
+ Type: AccountTypeOAuth,
+ Concurrency: 1,
+ Credentials: map[string]any{
+ "access_token": "oauth-token",
+ },
+ Status: StatusActive,
+ Schedulable: true,
+ }
+
+ result, err := svc.Forward(context.Background(), c, account, parsed)
+ require.NoError(t, err)
+ require.NotNil(t, result)
+
+ system := gjson.GetBytes(upstream.lastBody, "system")
+ require.True(t, system.Exists())
+ require.Equal(t, "Original system prompt", system.String())
+ require.NotContains(t, string(upstream.lastBody), "x-anthropic-billing-header:")
+ require.NotContains(t, string(upstream.lastBody), "[System Instructions]")
+}
+
func TestGatewayService_AnthropicAPIKeyPassthrough_StreamingStillCollectsUsageAfterClientDisconnect(t *testing.T) {
gin.SetMode(gin.TestMode)
diff --git a/backend/internal/service/gateway_billing_block.go b/backend/internal/service/gateway_billing_block.go
index 06a1a21c86..0a5128835a 100644
--- a/backend/internal/service/gateway_billing_block.go
+++ b/backend/internal/service/gateway_billing_block.go
@@ -3,7 +3,6 @@ package service
import (
"crypto/sha256"
"encoding/hex"
- "encoding/json"
"fmt"
"github.com/tidwall/gjson"
@@ -71,28 +70,24 @@ func extractFirstUserText(body []byte) string {
return first
}
-// buildBillingAttributionBlockJSON 构造 system 数组的 billing attribution block。
+// buildBillingAttributionText 构造 system 数组的 billing attribution 文本。
//
// 形态严格对齐真实 Claude Code CLI:
//
-// {"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.161.{fp}; cc_entrypoint=cli; cch=00000;"}
+// x-anthropic-billing-header: cc_version=2.1.161.{fp}; cc_entrypoint=cli; cch=00000;
//
// cch=00000 是签名占位符,由 signBillingHeaderCCH 在 buildUpstreamRequest 阶段
// 替换为基于完整 body 的 xxhash64 5 位十六进制摘要。
//
// 此 block 不带 cache_control(与真实 CLI 一致;cache breakpoint 由后续的
// Claude Code prompt block 承担)。
-func buildBillingAttributionBlockJSON(body []byte, cliVersion string) ([]byte, error) {
+func buildBillingAttributionText(body []byte, cliVersion string) (string, error) {
if cliVersion == "" {
- return nil, fmt.Errorf("cliVersion required")
+ return "", fmt.Errorf("cliVersion required")
}
fp := computeClaudeCodeFingerprint(body, cliVersion)
- text := fmt.Sprintf(
+ return fmt.Sprintf(
"x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=cli; cch=00000;",
cliVersion, fp,
- )
- return json.Marshal(map[string]string{
- "type": "text",
- "text": text,
- })
+ ), nil
}
diff --git a/backend/internal/service/gateway_prompt_test.go b/backend/internal/service/gateway_prompt_test.go
index eb6f58d6d6..ed702e3d06 100644
--- a/backend/internal/service/gateway_prompt_test.go
+++ b/backend/internal/service/gateway_prompt_test.go
@@ -5,7 +5,9 @@ import (
"strings"
"testing"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
"github.com/stretchr/testify/require"
+ "github.com/tidwall/gjson"
)
func TestIsClaudeCodeClient(t *testing.T) {
@@ -465,3 +467,42 @@ func TestRewriteSystemForNonClaudeCode(t *testing.T) {
})
}
}
+
+func TestRewriteSystemForNonClaudeCodeWithPrompt_UsesCustomExpansionPrompt(t *testing.T) {
+ body := []byte(`{"model":"claude-3","system":"Project instructions","messages":[{"role":"user","content":"hello"}]}`)
+ customPrompt := "Custom Claude OAuth expansion prompt"
+
+ result := rewriteSystemForNonClaudeCodeWithPrompt(body, "Project instructions", customPrompt)
+
+ system := gjson.GetBytes(result, "system")
+ require.True(t, system.IsArray())
+ require.Len(t, system.Array(), 3)
+ require.Equal(t, customPrompt, system.Array()[2].Get("text").String())
+ require.Equal(t, "ephemeral", system.Array()[2].Get("cache_control.type").String())
+}
+
+func TestRewriteSystemForNonClaudeCodeWithPromptBlocks_UsesConfiguredBlocks(t *testing.T) {
+ body := []byte(`{"model":"claude-3","system":"Project instructions","messages":[{"role":"user","content":"hello"}]}`)
+ blocks := `{
+ "blocks": [
+ {"type":"text","text":"prefix {cc_version}.{fp}","cache_control":true},
+ {"enabled":false,"type":"text","text":"disabled"},
+ {"type":"text","text":"{claude_code_system_prompt}"},
+ {"type":"text","text":"tail","cache_control":{"type":"ephemeral","ttl":"1h"}}
+ ]
+ }`
+
+ result := rewriteSystemForNonClaudeCodeWithPromptBlocks(body, "Project instructions", "", blocks)
+
+ system := gjson.GetBytes(result, "system")
+ require.True(t, system.IsArray())
+ arr := system.Array()
+ require.Len(t, arr, 3)
+ require.Contains(t, arr[0].Get("text").String(), "prefix "+claude.CLICurrentVersion+".")
+ require.Equal(t, "ephemeral", arr[0].Get("cache_control.type").String())
+ require.Equal(t, claude.DefaultCacheControlTTL, arr[0].Get("cache_control.ttl").String())
+ require.Equal(t, claudeCodeSystemPrompt, arr[1].Get("text").String())
+ require.False(t, arr[1].Get("cache_control").Exists())
+ require.Equal(t, "tail", arr[2].Get("text").String())
+ require.Equal(t, "1h", arr[2].Get("cache_control.ttl").String())
+}
diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go
index 82b57f3041..2ddd2f6788 100644
--- a/backend/internal/service/gateway_service.go
+++ b/backend/internal/service/gateway_service.go
@@ -28,6 +28,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
+ infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
@@ -1038,6 +1039,17 @@ func marshalAnthropicSystemTextBlock(text string, includeCacheControl bool) ([]b
return json.Marshal(block)
}
+func marshalAnthropicSystemTextBlockWithCacheControl(text string, cacheControl any) ([]byte, error) {
+ block := map[string]any{
+ "type": "text",
+ "text": text,
+ }
+ if cacheControl != nil {
+ block["cache_control"] = cacheControl
+ }
+ return json.Marshal(block)
+}
+
func marshalAnthropicMetadata(userID string) ([]byte, error) {
return json.Marshal(anthropicMetadataPayload{UserID: userID})
}
@@ -1336,9 +1348,10 @@ func (s *GatewayService) applyClaudeCodeOAuthMimicryToBody(
return body
}
+ systemPromptInjectionEnabled, systemPrompt, systemPromptBlocks := s.claudeOAuthSystemPromptInjectionSettings(ctx)
systemRewritten := false
- if !strings.Contains(strings.ToLower(model), "haiku") {
- body = rewriteSystemForNonClaudeCode(body, normalizeSystemParam(systemRaw))
+ if systemPromptInjectionEnabled && !strings.Contains(strings.ToLower(model), "haiku") {
+ body = rewriteSystemForNonClaudeCodeWithPromptBlocks(body, normalizeSystemParam(systemRaw), systemPrompt, systemPromptBlocks)
systemRewritten = true
}
@@ -4161,7 +4174,183 @@ func injectClaudeCodePrompt(body []byte, system any) []byte {
// 无法通过检测,因为后续内容仍为非 Claude Code 格式。
// 策略:将原始 system prompt 提取并注入为 user/assistant 消息对,system 仅保留 Claude Code 标识。
func rewriteSystemForNonClaudeCode(body []byte, system any) []byte {
+ return rewriteSystemForNonClaudeCodeWithPromptBlocks(body, system, "", "")
+}
+
+func rewriteSystemForNonClaudeCodeWithPrompt(body []byte, system any, expansionPrompt string) []byte {
+ return rewriteSystemForNonClaudeCodeWithPromptBlocks(body, system, expansionPrompt, "")
+}
+
+type claudeOAuthSystemPromptBlockConfig struct {
+ Enabled *bool `json:"enabled,omitempty"`
+ Type string `json:"type,omitempty"`
+ Text string `json:"text,omitempty"`
+ CacheControl json.RawMessage `json:"cache_control,omitempty"`
+}
+
+type claudeOAuthSystemPromptBlocksEnvelope struct {
+ Blocks []claudeOAuthSystemPromptBlockConfig `json:"blocks"`
+}
+
+func defaultClaudeOAuthExpansionPrompt(expansionPrompt string) string {
+ expansionPrompt = strings.TrimSpace(expansionPrompt)
+ if expansionPrompt == "" {
+ return claudeCodeSystemPromptExpansion
+ }
+ return expansionPrompt
+}
+
+func parseClaudeOAuthSystemPromptBlocksConfig(raw string) ([]claudeOAuthSystemPromptBlockConfig, error) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return nil, nil
+ }
+ if strings.HasPrefix(raw, "[") {
+ var blocks []claudeOAuthSystemPromptBlockConfig
+ if err := json.Unmarshal([]byte(raw), &blocks); err != nil {
+ return nil, err
+ }
+ return blocks, nil
+ }
+ var envelope claudeOAuthSystemPromptBlocksEnvelope
+ if err := json.Unmarshal([]byte(raw), &envelope); err != nil {
+ return nil, err
+ }
+ return envelope.Blocks, nil
+}
+
+func decodeClaudeOAuthSystemPromptCacheControl(raw json.RawMessage) (any, error) {
+ trimmed := bytes.TrimSpace(raw)
+ if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) || bytes.Equal(trimmed, []byte("false")) {
+ return nil, nil
+ }
+ if bytes.Equal(trimmed, []byte("true")) {
+ return map[string]string{
+ "type": "ephemeral",
+ "ttl": claude.DefaultCacheControlTTL,
+ }, nil
+ }
+ var value any
+ if err := json.Unmarshal(trimmed, &value); err != nil {
+ return nil, err
+ }
+ if _, ok := value.(map[string]any); !ok {
+ return nil, fmt.Errorf("cache_control must be boolean, null, or object")
+ }
+ return value, nil
+}
+
+func expandClaudeOAuthSystemPromptTextTemplate(body []byte, text string, expansionPrompt string) (string, error) {
+ if text == "" {
+ return "", nil
+ }
+ expansionPrompt = defaultClaudeOAuthExpansionPrompt(expansionPrompt)
+ billingText, err := buildBillingAttributionText(body, claude.CLICurrentVersion)
+ if err != nil {
+ return "", err
+ }
+ fp := computeClaudeCodeFingerprint(body, claude.CLICurrentVersion)
+ replacer := strings.NewReplacer(
+ "{billing_header}", billingText,
+ "{cc_version}", claude.CLICurrentVersion,
+ "{fp}", fp,
+ "{claude_code_system_prompt}", claudeCodeSystemPrompt,
+ "{claude_code_expansion_prompt}", expansionPrompt,
+ )
+ return replacer.Replace(text), nil
+}
+
+func defaultClaudeOAuthSystemPromptBlockConfig() []claudeOAuthSystemPromptBlockConfig {
+ enabled := true
+ return []claudeOAuthSystemPromptBlockConfig{
+ {
+ Enabled: &enabled,
+ Type: "text",
+ Text: "{billing_header}",
+ },
+ {
+ Enabled: &enabled,
+ Type: "text",
+ Text: "{claude_code_system_prompt}",
+ },
+ {
+ Enabled: &enabled,
+ Type: "text",
+ Text: "{claude_code_expansion_prompt}",
+ CacheControl: json.RawMessage(
+ fmt.Sprintf(`{"type":"ephemeral","ttl":%q}`, claude.DefaultCacheControlTTL),
+ ),
+ },
+ }
+}
+
+func buildClaudeOAuthSystemPromptBlocksJSON(body []byte, expansionPrompt string, blocksConfig string) ([][]byte, error) {
+ blocks, err := parseClaudeOAuthSystemPromptBlocksConfig(blocksConfig)
+ if err != nil {
+ return nil, err
+ }
+ if len(blocks) == 0 {
+ blocks = defaultClaudeOAuthSystemPromptBlockConfig()
+ }
+
+ items := make([][]byte, 0, len(blocks))
+ for i, block := range blocks {
+ if block.Enabled != nil && !*block.Enabled {
+ continue
+ }
+ blockType := strings.TrimSpace(block.Type)
+ if blockType == "" {
+ blockType = "text"
+ }
+ if blockType != "text" {
+ return nil, fmt.Errorf("system block %d type %q is not supported", i, block.Type)
+ }
+ text, err := expandClaudeOAuthSystemPromptTextTemplate(body, block.Text, expansionPrompt)
+ if err != nil {
+ return nil, err
+ }
+ if strings.TrimSpace(text) == "" {
+ continue
+ }
+ cacheControl, err := decodeClaudeOAuthSystemPromptCacheControl(block.CacheControl)
+ if err != nil {
+ return nil, fmt.Errorf("system block %d cache_control: %w", i, err)
+ }
+ raw, err := marshalAnthropicSystemTextBlockWithCacheControl(text, cacheControl)
+ if err != nil {
+ return nil, err
+ }
+ items = append(items, raw)
+ }
+ return items, nil
+}
+
+func ValidateClaudeOAuthSystemPromptBlocksConfig(raw string) error {
+ if strings.TrimSpace(raw) == "" {
+ return nil
+ }
+ blocks, err := parseClaudeOAuthSystemPromptBlocksConfig(raw)
+ if err != nil {
+ return infraerrors.BadRequest("INVALID_CLAUDE_OAUTH_SYSTEM_PROMPT_BLOCKS", "claude oauth system prompt blocks must be valid JSON")
+ }
+ for i, block := range blocks {
+ blockType := strings.TrimSpace(block.Type)
+ if blockType == "" {
+ blockType = "text"
+ }
+ if blockType != "text" {
+ return infraerrors.BadRequest("INVALID_CLAUDE_OAUTH_SYSTEM_PROMPT_BLOCKS", fmt.Sprintf("system block %d type must be text", i))
+ }
+ if _, err := decodeClaudeOAuthSystemPromptCacheControl(block.CacheControl); err != nil {
+ return infraerrors.BadRequest("INVALID_CLAUDE_OAUTH_SYSTEM_PROMPT_BLOCKS", fmt.Sprintf("system block %d cache_control is invalid", i))
+ }
+ }
+ return nil
+}
+
+func rewriteSystemForNonClaudeCodeWithPromptBlocks(body []byte, system any, expansionPrompt string, blocksConfig string) []byte {
system = normalizeSystemParam(system)
+ expansionPrompt = defaultClaudeOAuthExpansionPrompt(expansionPrompt)
// 1. 提取原始 system prompt 文本
var originalSystemText string
@@ -4182,7 +4371,7 @@ func rewriteSystemForNonClaudeCode(body []byte, system any) []byte {
// 2. 构造 system 数组,对齐真实 Claude Code CLI 的 3-block 形态:
// [0] billing attribution block(cc_version={cliVer}.{fp}; cc_entrypoint=cli; cch=00000;)
- // [1] "You are Claude Code..." 身份前缀 block(带 cache_control)
+ // [1] "You are Claude Code..." 身份前缀 block(默认不带 cache_control)
// [2] 工具无关的通用提示词扩充 block(带 cache_control 作为稳定缓存断点)
//
// 真实 CC 的 system 在身份前缀之后还有大段提示词,仅有 2 块会在块数/体量上明显
@@ -4192,16 +4381,16 @@ func rewriteSystemForNonClaudeCode(body []byte, system any) []byte {
// billing block 的 cch=00000 是占位符,会被 buildUpstreamRequest 里的
// signBillingHeaderCCH 替换成 xxhash64 签名。缺失 billing block 的系统 payload
// 是 Anthropic 判定第三方的关键信号之一(真实 CLI 每个请求都带)。
- billingBlock, billingErr := buildBillingAttributionBlockJSON(body, claude.CLICurrentVersion)
- // 身份块不带 cache_control;缓存断点统一落在最后一个静态块(扩充块)上,
- // 使 billing+身份+扩充 整段静态前缀都被同一断点覆盖,且只消耗 1 个断点配额。
- ccPromptBlock, ccErr := marshalAnthropicSystemTextBlock(claudeCodeSystemPrompt, false)
- ccExpansionBlock, expErr := marshalAnthropicSystemTextBlock(claudeCodeSystemPromptExpansion, true)
- if billingErr != nil || ccErr != nil || expErr != nil {
- logger.LegacyPrintf("service.gateway", "Warning: failed to build system blocks (billing=%v, cc=%v, exp=%v)", billingErr, ccErr, expErr)
+ systemBlocks, blockErr := buildClaudeOAuthSystemPromptBlocksJSON(body, expansionPrompt, blocksConfig)
+ if blockErr != nil {
+ logger.LegacyPrintf("service.gateway", "Warning: failed to build configured Claude OAuth system blocks: %v", blockErr)
+ systemBlocks, blockErr = buildClaudeOAuthSystemPromptBlocksJSON(body, expansionPrompt, "")
+ }
+ if blockErr != nil {
+ logger.LegacyPrintf("service.gateway", "Warning: failed to build default Claude OAuth system blocks: %v", blockErr)
return body
}
- out, ok := setJSONRawBytes(body, "system", buildJSONArrayRaw([][]byte{billingBlock, ccPromptBlock, ccExpansionBlock}))
+ out, ok := setJSONRawBytes(body, "system", buildJSONArrayRaw(systemBlocks))
if !ok {
logger.LegacyPrintf("service.gateway", "Warning: failed to set Claude Code system prompt")
return body
@@ -4481,6 +4670,13 @@ func (s *GatewayService) shouldInjectAnthropicCacheTTL1h(ctx context.Context, ac
return s.settingService.IsAnthropicCacheTTL1hInjectionEnabled(ctx)
}
+func (s *GatewayService) claudeOAuthSystemPromptInjectionSettings(ctx context.Context) (bool, string, string) {
+ if s == nil || s.settingService == nil {
+ return true, "", ""
+ }
+ return s.settingService.GetClaudeOAuthSystemPromptInjectionSettings(ctx)
+}
+
// Forward 转发请求到Claude API
func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) (*ForwardResult, error) {
startTime := time.Now()
@@ -4572,14 +4768,17 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
systemRewritten := false
if !strings.Contains(strings.ToLower(reqModel), "haiku") {
systemRaw, _ := parsed.SystemValue()
- if err := replaceBody(rewriteSystemForNonClaudeCode(body, systemRaw)); err != nil {
- return nil, err
+ systemPromptInjectionEnabled, systemPrompt, systemPromptBlocks := s.claudeOAuthSystemPromptInjectionSettings(ctx)
+ if systemPromptInjectionEnabled {
+ if err := replaceBody(rewriteSystemForNonClaudeCodeWithPromptBlocks(body, systemRaw, systemPrompt, systemPromptBlocks)); err != nil {
+ return nil, err
+ }
+ systemRewritten = true
}
- systemRewritten = true
}
// system 被重写时保留 CC prompt 的 cache_control: ephemeral(匹配真实 Claude Code 行为);
- // 未重写时(haiku / 已含 CC 前缀)剥离客户端 cache_control,与原有行为一致。
+ // 未重写时(haiku / 注入开关关闭)剥离客户端 cache_control,与原有行为一致。
// 两种情况下 enforceCacheControlLimit 都会兜底处理上限。
normalizeOpts := claudeOAuthNormalizeOptions{stripSystemCacheControl: !systemRewritten}
if s.identityService != nil {
diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go
index 7043736a80..4e72797fa9 100644
--- a/backend/internal/service/setting_service.go
+++ b/backend/internal/service/setting_service.go
@@ -103,12 +103,15 @@ const backendModeDBTimeout = 5 * time.Second
// cachedGatewayForwardingSettings 缓存网关转发行为设置(进程内缓存,60s TTL)
type cachedGatewayForwardingSettings struct {
- fingerprintUnification bool
- metadataPassthrough bool
- cchSigning bool
- anthropicCacheTTL1hInjection bool
- rewriteMessageCacheControl bool
- expiresAt int64 // unix nano
+ fingerprintUnification bool
+ metadataPassthrough bool
+ cchSigning bool
+ claudeOAuthSystemPromptInjection bool
+ claudeOAuthSystemPrompt string
+ claudeOAuthSystemPromptBlocks string
+ anthropicCacheTTL1hInjection bool
+ rewriteMessageCacheControl bool
+ expiresAt int64 // unix nano
}
var gatewayForwardingCache atomic.Value // *cachedGatewayForwardingSettings
@@ -1909,6 +1912,12 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingKeyEnableFingerprintUnification] = strconv.FormatBool(settings.EnableFingerprintUnification)
updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough)
updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning)
+ updates[SettingKeyEnableClaudeOAuthSystemPromptInjection] = strconv.FormatBool(settings.EnableClaudeOAuthSystemPromptInjection)
+ updates[SettingKeyClaudeOAuthSystemPrompt] = settings.ClaudeOAuthSystemPrompt
+ if err := ValidateClaudeOAuthSystemPromptBlocksConfig(settings.ClaudeOAuthSystemPromptBlocks); err != nil {
+ return nil, err
+ }
+ updates[SettingKeyClaudeOAuthSystemPromptBlocks] = settings.ClaudeOAuthSystemPromptBlocks
updates[SettingKeyEnableAnthropicCacheTTL1hInjection] = strconv.FormatBool(settings.EnableAnthropicCacheTTL1hInjection)
updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl)
updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
@@ -2035,12 +2044,15 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
})
gatewayForwardingSF.Forget("gateway_forwarding")
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
- fingerprintUnification: settings.EnableFingerprintUnification,
- metadataPassthrough: settings.EnableMetadataPassthrough,
- cchSigning: settings.EnableCCHSigning,
- anthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
- rewriteMessageCacheControl: settings.RewriteMessageCacheControl,
- expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
+ fingerprintUnification: settings.EnableFingerprintUnification,
+ metadataPassthrough: settings.EnableMetadataPassthrough,
+ cchSigning: settings.EnableCCHSigning,
+ claudeOAuthSystemPromptInjection: settings.EnableClaudeOAuthSystemPromptInjection,
+ claudeOAuthSystemPrompt: settings.ClaudeOAuthSystemPrompt,
+ claudeOAuthSystemPromptBlocks: settings.ClaudeOAuthSystemPromptBlocks,
+ anthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
+ rewriteMessageCacheControl: settings.RewriteMessageCacheControl,
+ expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
})
s.antigravityUAVersionSF.Forget("antigravity_user_agent_version")
antigravityUserAgentVersion := antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
@@ -2244,18 +2256,22 @@ func (s *SettingService) IsBackendModeEnabled(ctx context.Context) bool {
}
type gatewayForwardingSettingsResult struct {
- fp, mp, cch, cacheTTL1h, rewriteMessageCacheControl bool
+ fp, mp, cch, claudeOAuthSystemPromptInjection, cacheTTL1h, rewriteMessageCacheControl bool
+ claudeOAuthSystemPrompt, claudeOAuthSystemPromptBlocks string
}
func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context) gatewayForwardingSettingsResult {
if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil {
if time.Now().UnixNano() < cached.expiresAt {
return gatewayForwardingSettingsResult{
- fp: cached.fingerprintUnification,
- mp: cached.metadataPassthrough,
- cch: cached.cchSigning,
- cacheTTL1h: cached.anthropicCacheTTL1hInjection,
- rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
+ fp: cached.fingerprintUnification,
+ mp: cached.metadataPassthrough,
+ cch: cached.cchSigning,
+ claudeOAuthSystemPromptInjection: cached.claudeOAuthSystemPromptInjection,
+ claudeOAuthSystemPrompt: cached.claudeOAuthSystemPrompt,
+ claudeOAuthSystemPromptBlocks: cached.claudeOAuthSystemPromptBlocks,
+ cacheTTL1h: cached.anthropicCacheTTL1hInjection,
+ rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
}
}
}
@@ -2263,11 +2279,14 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil {
if time.Now().UnixNano() < cached.expiresAt {
return gatewayForwardingSettingsResult{
- fp: cached.fingerprintUnification,
- mp: cached.metadataPassthrough,
- cch: cached.cchSigning,
- cacheTTL1h: cached.anthropicCacheTTL1hInjection,
- rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
+ fp: cached.fingerprintUnification,
+ mp: cached.metadataPassthrough,
+ cch: cached.cchSigning,
+ claudeOAuthSystemPromptInjection: cached.claudeOAuthSystemPromptInjection,
+ claudeOAuthSystemPrompt: cached.claudeOAuthSystemPrompt,
+ claudeOAuthSystemPromptBlocks: cached.claudeOAuthSystemPromptBlocks,
+ cacheTTL1h: cached.anthropicCacheTTL1hInjection,
+ rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
}, nil
}
}
@@ -2277,20 +2296,24 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
SettingKeyEnableFingerprintUnification,
SettingKeyEnableMetadataPassthrough,
SettingKeyEnableCCHSigning,
+ SettingKeyEnableClaudeOAuthSystemPromptInjection,
+ SettingKeyClaudeOAuthSystemPrompt,
+ SettingKeyClaudeOAuthSystemPromptBlocks,
SettingKeyEnableAnthropicCacheTTL1hInjection,
SettingKeyRewriteMessageCacheControl,
})
if err != nil {
slog.Warn("failed to get gateway forwarding settings", "error", err)
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
- fingerprintUnification: true,
- metadataPassthrough: false,
- cchSigning: false,
- anthropicCacheTTL1hInjection: false,
- rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl(),
- expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(),
+ fingerprintUnification: true,
+ metadataPassthrough: false,
+ cchSigning: false,
+ claudeOAuthSystemPromptInjection: true,
+ anthropicCacheTTL1hInjection: false,
+ rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl(),
+ expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(),
})
- return gatewayForwardingSettingsResult{fp: true, rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl()}, nil
+ return gatewayForwardingSettingsResult{fp: true, claudeOAuthSystemPromptInjection: true, rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl()}, nil
}
fp := true
if v, ok := values[SettingKeyEnableFingerprintUnification]; ok && v != "" {
@@ -2298,31 +2321,43 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
}
mp := values[SettingKeyEnableMetadataPassthrough] == "true"
cch := values[SettingKeyEnableCCHSigning] == "true"
+ systemPromptInjection := true
+ if v, ok := values[SettingKeyEnableClaudeOAuthSystemPromptInjection]; ok && v != "" {
+ systemPromptInjection = v == "true"
+ }
+ systemPrompt := values[SettingKeyClaudeOAuthSystemPrompt]
+ systemPromptBlocks := values[SettingKeyClaudeOAuthSystemPromptBlocks]
cacheTTL1h := values[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
rewriteMessageCacheControl := s.defaultRewriteMessageCacheControl()
if v, ok := values[SettingKeyRewriteMessageCacheControl]; ok && v != "" {
rewriteMessageCacheControl = v == "true"
}
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
- fingerprintUnification: fp,
- metadataPassthrough: mp,
- cchSigning: cch,
- anthropicCacheTTL1hInjection: cacheTTL1h,
- rewriteMessageCacheControl: rewriteMessageCacheControl,
- expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
+ fingerprintUnification: fp,
+ metadataPassthrough: mp,
+ cchSigning: cch,
+ claudeOAuthSystemPromptInjection: systemPromptInjection,
+ claudeOAuthSystemPrompt: systemPrompt,
+ claudeOAuthSystemPromptBlocks: systemPromptBlocks,
+ anthropicCacheTTL1hInjection: cacheTTL1h,
+ rewriteMessageCacheControl: rewriteMessageCacheControl,
+ expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
})
return gatewayForwardingSettingsResult{
- fp: fp,
- mp: mp,
- cch: cch,
- cacheTTL1h: cacheTTL1h,
- rewriteMessageCacheControl: rewriteMessageCacheControl,
+ fp: fp,
+ mp: mp,
+ cch: cch,
+ claudeOAuthSystemPromptInjection: systemPromptInjection,
+ claudeOAuthSystemPrompt: systemPrompt,
+ claudeOAuthSystemPromptBlocks: systemPromptBlocks,
+ cacheTTL1h: cacheTTL1h,
+ rewriteMessageCacheControl: rewriteMessageCacheControl,
}, nil
})
if r, ok := val.(gatewayForwardingSettingsResult); ok {
return r
}
- return gatewayForwardingSettingsResult{fp: true}
+ return gatewayForwardingSettingsResult{fp: true, claudeOAuthSystemPromptInjection: true}
}
// GetGatewayForwardingSettings returns cached gateway forwarding settings.
@@ -2343,6 +2378,14 @@ func (s *SettingService) IsRewriteMessageCacheControlEnabled(ctx context.Context
return s.getGatewayForwardingSettingsCached(ctx).rewriteMessageCacheControl
}
+// GetClaudeOAuthSystemPromptInjectionSettings returns the Claude OAuth mimic
+// system block switch, legacy custom expansion prompt, and configurable blocks JSON.
+// Empty values mean use the built-in Claude Code default blocks.
+func (s *SettingService) GetClaudeOAuthSystemPromptInjectionSettings(ctx context.Context) (enabled bool, prompt string, blocks string) {
+ result := s.getGatewayForwardingSettingsCached(ctx)
+ return result.claudeOAuthSystemPromptInjection, result.claudeOAuthSystemPrompt, result.claudeOAuthSystemPromptBlocks
+}
+
// IsEmailVerifyEnabled 检查是否开启邮件验证
func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool {
value, err := s.settingRepo.GetValue(ctx, SettingKeyEmailVerifyEnabled)
@@ -3335,7 +3378,8 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
// 分组隔离
result.AllowUngroupedKeyScheduling = settings[SettingKeyAllowUngroupedKeyScheduling] == "true"
- // Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false, cch_signing=false)
+ // Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false,
+ // cch_signing=false, claude_oauth_system_prompt_injection=true)
if v, ok := settings[SettingKeyEnableFingerprintUnification]; ok && v != "" {
result.EnableFingerprintUnification = v == "true"
} else {
@@ -3343,6 +3387,13 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
}
result.EnableMetadataPassthrough = settings[SettingKeyEnableMetadataPassthrough] == "true"
result.EnableCCHSigning = settings[SettingKeyEnableCCHSigning] == "true"
+ if v, ok := settings[SettingKeyEnableClaudeOAuthSystemPromptInjection]; ok && v != "" {
+ result.EnableClaudeOAuthSystemPromptInjection = v == "true"
+ } else {
+ result.EnableClaudeOAuthSystemPromptInjection = true
+ }
+ result.ClaudeOAuthSystemPrompt = settings[SettingKeyClaudeOAuthSystemPrompt]
+ result.ClaudeOAuthSystemPromptBlocks = settings[SettingKeyClaudeOAuthSystemPromptBlocks]
result.EnableAnthropicCacheTTL1hInjection = settings[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
if v, ok := settings[SettingKeyRewriteMessageCacheControl]; ok && v != "" {
result.RewriteMessageCacheControl = v == "true"
diff --git a/backend/internal/service/setting_service_claude_oauth_system_prompt_test.go b/backend/internal/service/setting_service_claude_oauth_system_prompt_test.go
new file mode 100644
index 0000000000..d4a10787e7
--- /dev/null
+++ b/backend/internal/service/setting_service_claude_oauth_system_prompt_test.go
@@ -0,0 +1,49 @@
+package service
+
+import (
+ "context"
+ "testing"
+
+ "github.com/Wei-Shaw/sub2api/internal/config"
+ "github.com/stretchr/testify/require"
+)
+
+func resetGatewayForwardingSettingsCacheForTest(t *testing.T) {
+ t.Helper()
+ gatewayForwardingSF.Forget("gateway_forwarding")
+ gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
+ t.Cleanup(func() {
+ gatewayForwardingSF.Forget("gateway_forwarding")
+ gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
+ })
+}
+
+func TestSettingService_GetClaudeOAuthSystemPromptInjectionSettings(t *testing.T) {
+ t.Run("defaults to enabled with empty prompt", func(t *testing.T) {
+ resetGatewayForwardingSettingsCacheForTest(t)
+ svc := NewSettingService(&gatewayTTLSettingRepo{data: map[string]string{}}, &config.Config{})
+
+ enabled, prompt, blocks := svc.GetClaudeOAuthSystemPromptInjectionSettings(context.Background())
+
+ require.True(t, enabled)
+ require.Empty(t, prompt)
+ require.Empty(t, blocks)
+ })
+
+ t.Run("uses configured switch prompt and blocks", func(t *testing.T) {
+ resetGatewayForwardingSettingsCacheForTest(t)
+ const customPrompt = "custom prompt\n\nkeep spacing"
+ const customBlocks = `[{"type":"text","text":"custom block","cache_control":true}]`
+ svc := NewSettingService(&gatewayTTLSettingRepo{data: map[string]string{
+ SettingKeyEnableClaudeOAuthSystemPromptInjection: "false",
+ SettingKeyClaudeOAuthSystemPrompt: customPrompt,
+ SettingKeyClaudeOAuthSystemPromptBlocks: customBlocks,
+ }}, &config.Config{})
+
+ enabled, prompt, blocks := svc.GetClaudeOAuthSystemPromptInjectionSettings(context.Background())
+
+ require.False(t, enabled)
+ require.Equal(t, customPrompt, prompt)
+ require.Equal(t, customBlocks, blocks)
+ })
+}
diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go
index 9721720213..7d7c0173c7 100644
--- a/backend/internal/service/settings_view.go
+++ b/backend/internal/service/settings_view.go
@@ -188,14 +188,17 @@ type SystemSettings struct {
BackendModeEnabled bool
// Gateway forwarding behavior
- EnableFingerprintUnification bool // 是否统一 OAuth 账号的指纹头(默认 true)
- EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false)
- EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false)
- EnableAnthropicCacheTTL1hInjection bool // 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
- RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false)
- AntigravityUserAgentVersion string // Antigravity 上游 User-Agent 版本号;空值使用配置/默认值
- OpenAICodexUserAgent string // OpenAI Codex 上游完整 User-Agent;空值使用内置默认
- OpenAIAllowClaudeCodeCodexPlugin bool // 全局开关:是否额外放行 Claude Code 的 Codex 插件(默认 false)
+ EnableFingerprintUnification bool // 是否统一 OAuth 账号的指纹头(默认 true)
+ EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false)
+ EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false)
+ EnableClaudeOAuthSystemPromptInjection bool // 是否对 Claude OAuth mimic 路径注入 Claude Code system blocks(默认 true)
+ ClaudeOAuthSystemPrompt string // Claude OAuth mimic 路径注入的通用扩展 system prompt;空值使用内置默认
+ ClaudeOAuthSystemPromptBlocks string // Claude OAuth mimic 路径注入的 system blocks JSON 配置;空值使用内置默认
+ EnableAnthropicCacheTTL1hInjection bool // 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
+ RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false)
+ AntigravityUserAgentVersion string // Antigravity 上游 User-Agent 版本号;空值使用配置/默认值
+ OpenAICodexUserAgent string // OpenAI Codex 上游完整 User-Agent;空值使用内置默认
+ OpenAIAllowClaudeCodeCodexPlugin bool // 全局开关:是否额外放行 Claude Code 的 Codex 插件(默认 false)
// Web Search Emulation
WebSearchEmulationEnabled bool // 是否启用 web search 模拟
diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts
index 5be6307609..0a287a5caa 100644
--- a/frontend/src/api/admin/settings.ts
+++ b/frontend/src/api/admin/settings.ts
@@ -556,6 +556,9 @@ export interface SystemSettings {
enable_fingerprint_unification: boolean;
enable_metadata_passthrough: boolean;
enable_cch_signing: boolean;
+ enable_claude_oauth_system_prompt_injection: boolean;
+ claude_oauth_system_prompt: string;
+ claude_oauth_system_prompt_blocks: string;
enable_anthropic_cache_ttl_1h_injection: boolean;
rewrite_message_cache_control: boolean;
antigravity_user_agent_version: string;
@@ -792,6 +795,9 @@ export interface UpdateSettingsRequest {
enable_fingerprint_unification?: boolean;
enable_metadata_passthrough?: boolean;
enable_cch_signing?: boolean;
+ enable_claude_oauth_system_prompt_injection?: boolean;
+ claude_oauth_system_prompt?: string;
+ claude_oauth_system_prompt_blocks?: string;
enable_anthropic_cache_ttl_1h_injection?: boolean;
rewrite_message_cache_control?: boolean;
antigravity_user_agent_version?: string;
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index f71d62a19e..f3ea5ccbe8 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -5712,6 +5712,30 @@ export default {
metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.',
cchSigning: 'CCH Signing',
cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.',
+ claudeOAuthSystemPromptInjection: 'Claude OAuth System Blocks',
+ claudeOAuthSystemPromptInjectionHint: 'Inject Claude Code-like system blocks for Claude OAuth requests from non-Claude-Code clients. Enabled by default.',
+ claudeOAuthSystemPrompt: 'Claude OAuth Expansion Prompt',
+ claudeOAuthSystemPromptPlaceholder: 'Leave empty to use the built-in Claude Code expansion prompt.',
+ claudeOAuthSystemPromptHint: 'Legacy compatibility: controls only the third injected system block.',
+ claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks',
+ claudeOAuthSystemPromptBlocksPlaceholder: 'Leave empty to use the built-in 3 blocks. Supports an array or {"blocks": [...]}.',
+ claudeOAuthSystemPromptBlocksHint: 'Each block is saved as JSON with enabled, type, text, and optional cache_control. {billing_header} stays dynamic per request; the Claude Code identity and expansion prompts can be edited directly or restored from presets.',
+ systemBlockTitle: 'System Block {index}',
+ systemBlockPreset: 'Preset',
+ systemBlockPresetBilling: 'Billing header',
+ systemBlockPresetIdentity: 'Claude Code identity',
+ systemBlockPresetExpansion: 'Claude Code expansion',
+ systemBlockPresetCustom: 'Custom',
+ systemBlockType: 'Type',
+ systemBlockTypeText: 'Text',
+ systemBlockText: 'Content',
+ systemBlockCacheControl: 'Cache control',
+ systemBlockHide: 'Hide block details',
+ systemBlockShow: 'Show block details',
+ addSystemBlock: 'Add block',
+ resetSystemBlocks: 'Reset defaults',
+ cacheTTL5m: '5 minutes',
+ cacheTTL1h: '1 hour',
anthropicCacheTTL1hInjection: 'Anthropic Cache TTL Injection',
anthropicCacheTTL1hInjectionHint: 'When enabled, existing ephemeral cache_control blocks in Anthropic OAuth/Setup Token request bodies are forced to 1h; response usage is billed back as 5m by default, with account-level TTL billing override taking priority.',
rewriteMessageCacheControl: 'Rewrite Message Cache Breakpoints',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index 996b38d6d9..4c589b2eee 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -5866,6 +5866,30 @@ export default {
metadataPassthroughHint: '透传客户端原始 metadata.user_id,不进行重写。可能提高上游缓存命中率。',
cchSigning: 'CCH 签名',
cchSigningHint: '对转发请求的 billing header 进行 CCH 哈希签名。关闭时保留原始占位符。',
+ claudeOAuthSystemPromptInjection: 'Claude OAuth System 注入',
+ claudeOAuthSystemPromptInjectionHint: '为非 Claude Code 客户端的 Claude OAuth 请求注入 Claude Code 形态的 system blocks。默认开启。',
+ claudeOAuthSystemPrompt: 'Claude OAuth 扩展提示词',
+ claudeOAuthSystemPromptPlaceholder: '留空时使用内置 Claude Code 扩展提示词。',
+ claudeOAuthSystemPromptHint: '兼容旧配置:仅控制第三个注入的 system block。',
+ claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks',
+ claudeOAuthSystemPromptBlocksPlaceholder: '留空时使用内置 3 个 blocks。支持数组或 {"blocks": [...]}。',
+ claudeOAuthSystemPromptBlocksHint: '每个 block 会保存为带 enabled、type、text、可选 cache_control 的 JSON。{billing_header} 会按请求动态生成;Claude Code 身份提示词和扩展提示词可直接编辑,也可用预设恢复默认值。',
+ systemBlockTitle: 'System Block {index}',
+ systemBlockPreset: '预设',
+ systemBlockPresetBilling: 'Billing Header',
+ systemBlockPresetIdentity: 'Claude Code 身份提示词',
+ systemBlockPresetExpansion: 'Claude Code 扩展提示词',
+ systemBlockPresetCustom: '自定义',
+ systemBlockType: '类型',
+ systemBlockTypeText: '文本',
+ systemBlockText: '内容',
+ systemBlockCacheControl: 'Cache Control',
+ systemBlockHide: '隐藏 block 详情',
+ systemBlockShow: '展示 block 详情',
+ addSystemBlock: '添加 block',
+ resetSystemBlocks: '恢复默认',
+ cacheTTL5m: '5 分钟',
+ cacheTTL1h: '1 小时',
anthropicCacheTTL1hInjection: 'Anthropic 缓存 TTL 注入',
anthropicCacheTTL1hInjectionHint: '开启后,对 Anthropic OAuth/Setup Token 请求体中已有的 ephemeral 缓存块强制写入 1h;响应 usage 默认按 5m 回写计费,账号级 TTL 计费设置优先。',
rewriteMessageCacheControl: '改写消息缓存断点',
diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue
index c3e0f2b965..1db5122062 100644
--- a/frontend/src/views/admin/SettingsView.vue
+++ b/frontend/src/views/admin/SettingsView.vue
@@ -3841,6 +3841,237 @@
+ {{ + t( + "admin.settings.gatewayForwarding.claudeOAuthSystemPromptInjectionHint", + ) + }} +
++ {{ + t( + "admin.settings.gatewayForwarding.claudeOAuthSystemPromptBlocksHint", + ) + }} +
+