fix: 让消息 cache_control 改写默认关闭
This commit is contained in:
@@ -225,6 +225,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|||||||
EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
|
EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
|
||||||
EnableCCHSigning: settings.EnableCCHSigning,
|
EnableCCHSigning: settings.EnableCCHSigning,
|
||||||
EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
|
EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
|
||||||
|
RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
|
||||||
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
|
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
|
||||||
PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
|
PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
|
||||||
PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
|
PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
|
||||||
@@ -515,6 +516,7 @@ type UpdateSettingsRequest struct {
|
|||||||
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
|
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
|
||||||
EnableCCHSigning *bool `json:"enable_cch_signing"`
|
EnableCCHSigning *bool `json:"enable_cch_signing"`
|
||||||
EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
|
EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
|
||||||
|
RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
|
||||||
|
|
||||||
// Payment visible method routing
|
// Payment visible method routing
|
||||||
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
|
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
|
||||||
@@ -1415,6 +1417,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return previousSettings.EnableAnthropicCacheTTL1hInjection
|
return previousSettings.EnableAnthropicCacheTTL1hInjection
|
||||||
}(),
|
}(),
|
||||||
|
RewriteMessageCacheControl: func() bool {
|
||||||
|
if req.RewriteMessageCacheControl != nil {
|
||||||
|
return *req.RewriteMessageCacheControl
|
||||||
|
}
|
||||||
|
return previousSettings.RewriteMessageCacheControl
|
||||||
|
}(),
|
||||||
PaymentVisibleMethodAlipaySource: func() string {
|
PaymentVisibleMethodAlipaySource: func() string {
|
||||||
if req.PaymentVisibleMethodAlipaySource != nil {
|
if req.PaymentVisibleMethodAlipaySource != nil {
|
||||||
return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
|
return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
|
||||||
@@ -1747,6 +1755,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
|
EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
|
||||||
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
||||||
EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
|
EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
|
||||||
|
RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
|
||||||
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
|
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
|
||||||
PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
|
PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
|
||||||
PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
|
PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
|
||||||
@@ -2143,6 +2152,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
|||||||
if before.EnableAnthropicCacheTTL1hInjection != after.EnableAnthropicCacheTTL1hInjection {
|
if before.EnableAnthropicCacheTTL1hInjection != after.EnableAnthropicCacheTTL1hInjection {
|
||||||
changed = append(changed, "enable_anthropic_cache_ttl_1h_injection")
|
changed = append(changed, "enable_anthropic_cache_ttl_1h_injection")
|
||||||
}
|
}
|
||||||
|
if before.RewriteMessageCacheControl != after.RewriteMessageCacheControl {
|
||||||
|
changed = append(changed, "rewrite_message_cache_control")
|
||||||
|
}
|
||||||
if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource {
|
if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource {
|
||||||
changed = append(changed, "payment_visible_method_alipay_source")
|
changed = append(changed, "payment_visible_method_alipay_source")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ type SystemSettings struct {
|
|||||||
EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
|
EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
|
||||||
EnableCCHSigning bool `json:"enable_cch_signing"`
|
EnableCCHSigning bool `json:"enable_cch_signing"`
|
||||||
EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"`
|
EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"`
|
||||||
|
RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
|
||||||
|
|
||||||
// Web Search Emulation
|
// Web Search Emulation
|
||||||
WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"`
|
WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"`
|
||||||
|
|||||||
@@ -773,6 +773,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"backend_mode_enabled": false,
|
"backend_mode_enabled": false,
|
||||||
"enable_cch_signing": false,
|
"enable_cch_signing": false,
|
||||||
"enable_anthropic_cache_ttl_1h_injection": false,
|
"enable_anthropic_cache_ttl_1h_injection": false,
|
||||||
|
"rewrite_message_cache_control": false,
|
||||||
"enable_fingerprint_unification": true,
|
"enable_fingerprint_unification": true,
|
||||||
"enable_metadata_passthrough": false,
|
"enable_metadata_passthrough": false,
|
||||||
"web_search_emulation_enabled": false,
|
"web_search_emulation_enabled": false,
|
||||||
@@ -988,6 +989,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"enable_metadata_passthrough": false,
|
"enable_metadata_passthrough": false,
|
||||||
"enable_cch_signing": false,
|
"enable_cch_signing": false,
|
||||||
"enable_anthropic_cache_ttl_1h_injection": false,
|
"enable_anthropic_cache_ttl_1h_injection": false,
|
||||||
|
"rewrite_message_cache_control": false,
|
||||||
"web_search_emulation_enabled": false,
|
"web_search_emulation_enabled": false,
|
||||||
"payment_visible_method_alipay_source": "",
|
"payment_visible_method_alipay_source": "",
|
||||||
"payment_visible_method_wxpay_source": "",
|
"payment_visible_method_wxpay_source": "",
|
||||||
|
|||||||
@@ -370,6 +370,8 @@ const (
|
|||||||
SettingKeyEnableCCHSigning = "enable_cch_signing"
|
SettingKeyEnableCCHSigning = "enable_cch_signing"
|
||||||
// SettingKeyEnableAnthropicCacheTTL1hInjection 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
|
// SettingKeyEnableAnthropicCacheTTL1hInjection 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
|
||||||
SettingKeyEnableAnthropicCacheTTL1hInjection = "enable_anthropic_cache_ttl_1h_injection"
|
SettingKeyEnableAnthropicCacheTTL1hInjection = "enable_anthropic_cache_ttl_1h_injection"
|
||||||
|
// SettingKeyRewriteMessageCacheControl 是否改写 messages[*].content[*].cache_control(默认 false)
|
||||||
|
SettingKeyRewriteMessageCacheControl = "rewrite_message_cache_control"
|
||||||
|
|
||||||
// Balance Low Notification
|
// Balance Low Notification
|
||||||
SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关
|
SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关
|
||||||
|
|||||||
@@ -150,6 +150,21 @@ func TestEnforceCacheControlLimit_PreservesTopLevelFieldOrder(t *testing.T) {
|
|||||||
require.Equal(t, 4, strings.Count(resultStr, `"cache_control"`))
|
require.Equal(t, 4, strings.Count(resultStr, `"cache_control"`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnforceCacheControlLimit_CountsToolsAndPreservesMessageAnchorsFirst(t *testing.T) {
|
||||||
|
body := []byte(`{"alpha":1,"system":[{"type":"text","text":"sys","cache_control":{"type":"ephemeral"}}],"messages":[{"role":"user","content":[{"type":"text","text":"m1","cache_control":{"type":"ephemeral"}},{"type":"text","text":"m2","cache_control":{"type":"ephemeral"}},{"type":"text","text":"m3","cache_control":{"type":"ephemeral"}}]}],"tools":[{"name":"a","input_schema":{},"cache_control":{"type":"ephemeral"}}],"omega":2}`)
|
||||||
|
|
||||||
|
result := enforceCacheControlLimit(body)
|
||||||
|
resultStr := string(result)
|
||||||
|
|
||||||
|
assertJSONTokenOrder(t, resultStr, `"alpha"`, `"system"`, `"messages"`, `"tools"`, `"omega"`)
|
||||||
|
require.Equal(t, 4, strings.Count(resultStr, `"cache_control"`))
|
||||||
|
require.True(t, gjson.GetBytes(result, "system.0.cache_control").Exists())
|
||||||
|
require.True(t, gjson.GetBytes(result, "messages.0.content.0.cache_control").Exists())
|
||||||
|
require.True(t, gjson.GetBytes(result, "messages.0.content.1.cache_control").Exists())
|
||||||
|
require.True(t, gjson.GetBytes(result, "messages.0.content.2.cache_control").Exists())
|
||||||
|
require.False(t, gjson.GetBytes(result, "tools.0.cache_control").Exists())
|
||||||
|
}
|
||||||
|
|
||||||
func TestInjectAnthropicCacheControlTTL1h_OnlyUpdatesExistingEphemeralCacheControl(t *testing.T) {
|
func TestInjectAnthropicCacheControlTTL1h_OnlyUpdatesExistingEphemeralCacheControl(t *testing.T) {
|
||||||
body := []byte(`{"alpha":1,"cache_control":{"type":"ephemeral"},"system":[{"type":"text","text":"sys","cache_control":{"type":"ephemeral","ttl":"5m"}},{"type":"text","text":"plain"}],"messages":[{"role":"user","content":[{"type":"text","text":"hi","cache_control":{"type":"ephemeral"}},{"type":"text","text":"non","cache_control":{"type":"persistent","ttl":"5m"}}]}],"tools":[{"name":"a","input_schema":{},"cache_control":{"type":"ephemeral"}}],"omega":2}`)
|
body := []byte(`{"alpha":1,"cache_control":{"type":"ephemeral"},"system":[{"type":"text","text":"sys","cache_control":{"type":"ephemeral","ttl":"5m"}},{"type":"text","text":"plain"}],"messages":[{"role":"user","content":[{"type":"text","text":"hi","cache_control":{"type":"ephemeral"}},{"type":"text","text":"non","cache_control":{"type":"persistent","ttl":"5m"}}]}],"tools":[{"name":"a","input_schema":{},"cache_control":{"type":"ephemeral"}}],"omega":2}`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
// stripMessageCacheControl 移除 $.messages[*].content[*].cache_control。
|
// stripMessageCacheControl 移除 $.messages[*].content[*].cache_control。
|
||||||
// 与 Parrot _strip_message_cache_control 语义一致。
|
// 与 Parrot _strip_message_cache_control 语义一致。
|
||||||
//
|
//
|
||||||
// 为什么必须整体清空:客户端(特别是 Claude Code)经常把 cache_control 打在
|
// 旧策略为什么整体清空:客户端(特别是 Claude Code)经常把 cache_control 打在
|
||||||
// "当前最后一条 user message" 上;下一轮对话 messages 追加后,原本的最后一条
|
// "当前最后一条 user message" 上;下一轮对话 messages 追加后,原本的最后一条
|
||||||
// 变成中间某条,cache_control 还挂着就导致"前缀签名变化",破坏缓存命中。
|
// 变成中间某条,cache_control 还挂着就导致"前缀签名变化",破坏缓存命中。
|
||||||
// 统一由代理重新打断点(addMessageCacheBreakpoints)才能在多轮间稳定。
|
// 统一由代理重新打断点(addMessageCacheBreakpoints)才能在多轮间稳定。
|
||||||
@@ -85,6 +86,25 @@ func addMessageCacheBreakpoints(body []byte) []byte {
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rewriteMessageCacheControlIfEnabled 按系统设置决定是否执行旧版 messages 缓存断点改写。
|
||||||
|
func (s *GatewayService) rewriteMessageCacheControlIfEnabled(ctx context.Context, body []byte) []byte {
|
||||||
|
if s == nil || !s.isRewriteMessageCacheControlEnabled(ctx) {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
body = stripMessageCacheControl(body)
|
||||||
|
return addMessageCacheBreakpoints(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GatewayService) isRewriteMessageCacheControlEnabled(ctx context.Context) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.settingService != nil {
|
||||||
|
return s.settingService.IsRewriteMessageCacheControlEnabled(ctx)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// injectCacheControlOnLastContentBlock 把 cache_control 断点打在 messages[idx]
|
// injectCacheControlOnLastContentBlock 把 cache_control 断点打在 messages[idx]
|
||||||
// 的最后一个 content block 上。若 content 是 string,先升级成单块 text 数组
|
// 的最后一个 content block 上。若 content 是 string,先升级成单块 text 数组
|
||||||
// (对齐 Parrot _inject_cache_on_msg 的行为)。
|
// (对齐 Parrot _inject_cache_on_msg 的行为)。
|
||||||
|
|||||||
@@ -1251,13 +1251,11 @@ func (s *GatewayService) applyClaudeCodeOAuthMimicryToBody(
|
|||||||
body, _ = normalizeClaudeOAuthRequestBody(body, model, normalizeOpts)
|
body, _ = normalizeClaudeOAuthRequestBody(body, model, normalizeOpts)
|
||||||
|
|
||||||
// Phase D+E+F: messages cache 策略 + 工具名混淆 + tools[-1] 断点
|
// Phase D+E+F: messages cache 策略 + 工具名混淆 + tools[-1] 断点
|
||||||
// 对齐 Parrot transform_request 里剩余的字段级改写。三步顺序有语义约束:
|
// 对齐 Parrot transform_request 里剩余的字段级改写。顺序有语义约束:
|
||||||
// 1) strip:先清除客户端的 messages[*].cache_control(多轮稳定性)
|
// 1) messages cache:仅在配置开启时清除客户端断点并注入代理断点
|
||||||
// 2) breakpoints:再注入 2 个断点(最后一条 + 倒数第二个 user turn)
|
// 2) tool rewrite:最后改 tools[*].name / tool_choice.name 并在 tools[-1]
|
||||||
// 3) tool rewrite:最后改 tools[*].name / tool_choice.name 并在 tools[-1]
|
|
||||||
// 上打断点;mapping 存入 gin.Context 供响应侧 bytes.Replace 还原。
|
// 上打断点;mapping 存入 gin.Context 供响应侧 bytes.Replace 还原。
|
||||||
body = stripMessageCacheControl(body)
|
body = s.rewriteMessageCacheControlIfEnabled(ctx, body)
|
||||||
body = addMessageCacheBreakpoints(body)
|
|
||||||
|
|
||||||
if rw := buildToolNameRewriteFromBody(body); rw != nil {
|
if rw := buildToolNameRewriteFromBody(body); rw != nil {
|
||||||
body = applyToolNameRewriteToBody(body, rw)
|
body = applyToolNameRewriteToBody(body, rw)
|
||||||
@@ -4108,7 +4106,7 @@ type cacheControlPath struct {
|
|||||||
log string
|
log string
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectCacheControlPaths(body []byte) (invalidThinking []cacheControlPath, messagePaths []string, systemPaths []string) {
|
func collectCacheControlPaths(body []byte) (invalidThinking []cacheControlPath, messagePaths []string, toolPaths []string, systemPaths []string) {
|
||||||
system := gjson.GetBytes(body, "system")
|
system := gjson.GetBytes(body, "system")
|
||||||
if system.IsArray() {
|
if system.IsArray() {
|
||||||
sysIndex := 0
|
sysIndex := 0
|
||||||
@@ -4157,17 +4155,29 @@ func collectCacheControlPaths(body []byte) (invalidThinking []cacheControlPath,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return invalidThinking, messagePaths, systemPaths
|
tools := gjson.GetBytes(body, "tools")
|
||||||
|
if tools.IsArray() {
|
||||||
|
toolIndex := 0
|
||||||
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
|
if tool.Get("cache_control").Exists() {
|
||||||
|
toolPaths = append(toolPaths, fmt.Sprintf("tools.%d.cache_control", toolIndex))
|
||||||
|
}
|
||||||
|
toolIndex++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return invalidThinking, messagePaths, toolPaths, systemPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
// enforceCacheControlLimit 强制执行 cache_control 块数量限制(最多 4 个)
|
// enforceCacheControlLimit 强制执行 cache_control 块数量限制(最多 4 个)
|
||||||
// 超限时优先从 messages 中移除 cache_control,保护 system 中的缓存控制
|
// 超限时优先移除工具断点,再移除 messages 断点,最后才移除 system 断点。
|
||||||
func enforceCacheControlLimit(body []byte) []byte {
|
func enforceCacheControlLimit(body []byte) []byte {
|
||||||
if len(body) == 0 {
|
if len(body) == 0 {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidThinking, messagePaths, systemPaths := collectCacheControlPaths(body)
|
invalidThinking, messagePaths, toolPaths, systemPaths := collectCacheControlPaths(body)
|
||||||
out := body
|
out := body
|
||||||
modified := false
|
modified := false
|
||||||
|
|
||||||
@@ -4185,7 +4195,7 @@ func enforceCacheControlLimit(body []byte) []byte {
|
|||||||
logger.LegacyPrintf("service.gateway", "%s", item.log)
|
logger.LegacyPrintf("service.gateway", "%s", item.log)
|
||||||
}
|
}
|
||||||
|
|
||||||
count := len(messagePaths) + len(systemPaths)
|
count := len(messagePaths) + len(toolPaths) + len(systemPaths)
|
||||||
if count <= maxCacheControlBlocks {
|
if count <= maxCacheControlBlocks {
|
||||||
if modified {
|
if modified {
|
||||||
return out
|
return out
|
||||||
@@ -4193,8 +4203,22 @@ func enforceCacheControlLimit(body []byte) []byte {
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
// 超限:优先从 messages 中移除,再从 system 中移除
|
// 超限:优先从 tools 中移除,再从 messages 中移除,最后才从 system 中移除。
|
||||||
remaining := count - maxCacheControlBlocks
|
remaining := count - maxCacheControlBlocks
|
||||||
|
for i := len(toolPaths) - 1; i >= 0 && remaining > 0; i-- {
|
||||||
|
path := toolPaths[i]
|
||||||
|
if !gjson.GetBytes(out, path).Exists() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next, ok := deleteJSONPathBytes(out, path)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = next
|
||||||
|
modified = true
|
||||||
|
remaining--
|
||||||
|
}
|
||||||
|
|
||||||
for _, path := range messagePaths {
|
for _, path := range messagePaths {
|
||||||
if remaining <= 0 {
|
if remaining <= 0 {
|
||||||
break
|
break
|
||||||
@@ -4418,11 +4442,10 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
|||||||
|
|
||||||
body, reqModel = normalizeClaudeOAuthRequestBody(body, reqModel, normalizeOpts)
|
body, reqModel = normalizeClaudeOAuthRequestBody(body, reqModel, normalizeOpts)
|
||||||
|
|
||||||
// D/E/F: messages cache 策略 + 工具名混淆 + tools[-1] 断点
|
// D/E/F: 可选 messages cache 策略 + 工具名混淆 + tools[-1] 断点
|
||||||
// 与 forward_as_chat_completions / forward_as_responses 路径对齐,
|
// 与 forward_as_chat_completions / forward_as_responses 路径对齐,
|
||||||
// 保证原生 /v1/messages 路径也经过完整的 Parrot 字段级改写。
|
// 原生 /v1/messages 路径也走同一套可配置字段级改写。
|
||||||
body = stripMessageCacheControl(body)
|
body = s.rewriteMessageCacheControlIfEnabled(ctx, body)
|
||||||
body = addMessageCacheBreakpoints(body)
|
|
||||||
if rw := buildToolNameRewriteFromBody(body); rw != nil {
|
if rw := buildToolNameRewriteFromBody(body); rw != nil {
|
||||||
body = applyToolNameRewriteToBody(body, rw)
|
body = applyToolNameRewriteToBody(body, rw)
|
||||||
c.Set(toolNameRewriteKey, rw)
|
c.Set(toolNameRewriteKey, rw)
|
||||||
@@ -8819,8 +8842,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
|
|||||||
normalizeOpts := claudeOAuthNormalizeOptions{stripSystemCacheControl: true}
|
normalizeOpts := claudeOAuthNormalizeOptions{stripSystemCacheControl: true}
|
||||||
body, reqModel = normalizeClaudeOAuthRequestBody(body, reqModel, normalizeOpts)
|
body, reqModel = normalizeClaudeOAuthRequestBody(body, reqModel, normalizeOpts)
|
||||||
|
|
||||||
body = stripMessageCacheControl(body)
|
body = s.rewriteMessageCacheControlIfEnabled(ctx, body)
|
||||||
body = addMessageCacheBreakpoints(body)
|
|
||||||
if rw := buildToolNameRewriteFromBody(body); rw != nil {
|
if rw := buildToolNameRewriteFromBody(body); rw != nil {
|
||||||
body = applyToolNameRewriteToBody(body, rw)
|
body = applyToolNameRewriteToBody(body, rw)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
@@ -188,6 +190,40 @@ func TestAddMessageCacheBreakpoints_StringContentPromoted(t *testing.T) {
|
|||||||
require.Equal(t, "5m", gjson.GetBytes(out, "messages.0.content.0.cache_control.ttl").String())
|
require.Equal(t, "5m", gjson.GetBytes(out, "messages.0.content.0.cache_control.ttl").String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRewriteMessageCacheControlIfEnabled_DefaultKeepsClientAnchors(t *testing.T) {
|
||||||
|
body := []byte(`{"messages":[
|
||||||
|
{"role":"user","content":[{"type":"text","text":"stable","cache_control":{"type":"ephemeral","ttl":"1h"}}]},
|
||||||
|
{"role":"assistant","content":[{"type":"text","text":"ok"}]},
|
||||||
|
{"role":"user","content":[{"type":"text","text":"latest","cache_control":{"type":"ephemeral","ttl":"5m"}}]}
|
||||||
|
]}`)
|
||||||
|
|
||||||
|
out := (&GatewayService{}).rewriteMessageCacheControlIfEnabled(context.Background(), body)
|
||||||
|
|
||||||
|
require.JSONEq(t, string(body), string(out))
|
||||||
|
require.Equal(t, "1h", gjson.GetBytes(out, "messages.0.content.0.cache_control.ttl").String())
|
||||||
|
require.Equal(t, "5m", gjson.GetBytes(out, "messages.2.content.0.cache_control.ttl").String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteMessageCacheControlIfEnabled_OptInPreservesLegacyRewrite(t *testing.T) {
|
||||||
|
body := []byte(`{"messages":[
|
||||||
|
{"role":"user","content":[{"type":"text","text":"stable","cache_control":{"type":"ephemeral","ttl":"1h"}}]},
|
||||||
|
{"role":"assistant","content":[{"type":"text","text":"ok"}]},
|
||||||
|
{"role":"user","content":[{"type":"text","text":"latest","cache_control":{"type":"ephemeral","ttl":"1h"}}]},
|
||||||
|
{"role":"assistant","content":[{"type":"text","text":"done"}]}
|
||||||
|
]}`)
|
||||||
|
repo := &gatewayTTLSettingRepo{data: map[string]string{
|
||||||
|
SettingKeyRewriteMessageCacheControl: "true",
|
||||||
|
}}
|
||||||
|
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{})
|
||||||
|
svc := &GatewayService{settingService: NewSettingService(repo, &config.Config{})}
|
||||||
|
|
||||||
|
out := svc.rewriteMessageCacheControlIfEnabled(context.Background(), body)
|
||||||
|
|
||||||
|
require.Equal(t, "5m", gjson.GetBytes(out, "messages.0.content.0.cache_control.ttl").String())
|
||||||
|
require.False(t, gjson.GetBytes(out, "messages.2.content.0.cache_control").Exists())
|
||||||
|
require.Equal(t, "5m", gjson.GetBytes(out, "messages.3.content.0.cache_control.ttl").String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildToolNameRewriteFromBody_ReverseOrderedByLengthDesc(t *testing.T) {
|
func TestBuildToolNameRewriteFromBody_ReverseOrderedByLengthDesc(t *testing.T) {
|
||||||
// 超过阈值触发动态映射,验证 ReverseOrdered 按假名长度倒序排列
|
// 超过阈值触发动态映射,验证 ReverseOrdered 按假名长度倒序排列
|
||||||
body := []byte(`{"tools":[
|
body := []byte(`{"tools":[
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ type cachedGatewayForwardingSettings struct {
|
|||||||
metadataPassthrough bool
|
metadataPassthrough bool
|
||||||
cchSigning bool
|
cchSigning bool
|
||||||
anthropicCacheTTL1hInjection bool
|
anthropicCacheTTL1hInjection bool
|
||||||
|
rewriteMessageCacheControl bool
|
||||||
expiresAt int64 // unix nano
|
expiresAt int64 // unix nano
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1584,6 +1585,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
|||||||
updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough)
|
updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough)
|
||||||
updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning)
|
updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning)
|
||||||
updates[SettingKeyEnableAnthropicCacheTTL1hInjection] = strconv.FormatBool(settings.EnableAnthropicCacheTTL1hInjection)
|
updates[SettingKeyEnableAnthropicCacheTTL1hInjection] = strconv.FormatBool(settings.EnableAnthropicCacheTTL1hInjection)
|
||||||
|
updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl)
|
||||||
updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource
|
updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource
|
||||||
updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource
|
updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource
|
||||||
updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
|
updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
|
||||||
@@ -1652,6 +1654,7 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
|
|||||||
metadataPassthrough: settings.EnableMetadataPassthrough,
|
metadataPassthrough: settings.EnableMetadataPassthrough,
|
||||||
cchSigning: settings.EnableCCHSigning,
|
cchSigning: settings.EnableCCHSigning,
|
||||||
anthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
|
anthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
|
||||||
|
rewriteMessageCacheControl: settings.RewriteMessageCacheControl,
|
||||||
expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
|
expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
|
||||||
})
|
})
|
||||||
openAIAdvancedSchedulerSettingSF.Forget(openAIAdvancedSchedulerSettingKey)
|
openAIAdvancedSchedulerSettingSF.Forget(openAIAdvancedSchedulerSettingKey)
|
||||||
@@ -1664,6 +1667,10 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) defaultRewriteMessageCacheControl() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) validateDefaultSubscriptionGroups(ctx context.Context, items []DefaultSubscriptionSetting) error {
|
func (s *SettingService) validateDefaultSubscriptionGroups(ctx context.Context, items []DefaultSubscriptionSetting) error {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -1815,7 +1822,7 @@ func (s *SettingService) IsBackendModeEnabled(ctx context.Context) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type gatewayForwardingSettingsResult struct {
|
type gatewayForwardingSettingsResult struct {
|
||||||
fp, mp, cch, cacheTTL1h bool
|
fp, mp, cch, cacheTTL1h, rewriteMessageCacheControl bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context) gatewayForwardingSettingsResult {
|
func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context) gatewayForwardingSettingsResult {
|
||||||
@@ -1826,6 +1833,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
|||||||
mp: cached.metadataPassthrough,
|
mp: cached.metadataPassthrough,
|
||||||
cch: cached.cchSigning,
|
cch: cached.cchSigning,
|
||||||
cacheTTL1h: cached.anthropicCacheTTL1hInjection,
|
cacheTTL1h: cached.anthropicCacheTTL1hInjection,
|
||||||
|
rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1837,6 +1845,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
|||||||
mp: cached.metadataPassthrough,
|
mp: cached.metadataPassthrough,
|
||||||
cch: cached.cchSigning,
|
cch: cached.cchSigning,
|
||||||
cacheTTL1h: cached.anthropicCacheTTL1hInjection,
|
cacheTTL1h: cached.anthropicCacheTTL1hInjection,
|
||||||
|
rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1847,6 +1856,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
|||||||
SettingKeyEnableMetadataPassthrough,
|
SettingKeyEnableMetadataPassthrough,
|
||||||
SettingKeyEnableCCHSigning,
|
SettingKeyEnableCCHSigning,
|
||||||
SettingKeyEnableAnthropicCacheTTL1hInjection,
|
SettingKeyEnableAnthropicCacheTTL1hInjection,
|
||||||
|
SettingKeyRewriteMessageCacheControl,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to get gateway forwarding settings", "error", err)
|
slog.Warn("failed to get gateway forwarding settings", "error", err)
|
||||||
@@ -1855,9 +1865,10 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
|||||||
metadataPassthrough: false,
|
metadataPassthrough: false,
|
||||||
cchSigning: false,
|
cchSigning: false,
|
||||||
anthropicCacheTTL1hInjection: false,
|
anthropicCacheTTL1hInjection: false,
|
||||||
|
rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl(),
|
||||||
expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(),
|
expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(),
|
||||||
})
|
})
|
||||||
return gatewayForwardingSettingsResult{fp: true}, nil
|
return gatewayForwardingSettingsResult{fp: true, rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl()}, nil
|
||||||
}
|
}
|
||||||
fp := true
|
fp := true
|
||||||
if v, ok := values[SettingKeyEnableFingerprintUnification]; ok && v != "" {
|
if v, ok := values[SettingKeyEnableFingerprintUnification]; ok && v != "" {
|
||||||
@@ -1866,14 +1877,25 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
|||||||
mp := values[SettingKeyEnableMetadataPassthrough] == "true"
|
mp := values[SettingKeyEnableMetadataPassthrough] == "true"
|
||||||
cch := values[SettingKeyEnableCCHSigning] == "true"
|
cch := values[SettingKeyEnableCCHSigning] == "true"
|
||||||
cacheTTL1h := values[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
|
cacheTTL1h := values[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
|
||||||
|
rewriteMessageCacheControl := s.defaultRewriteMessageCacheControl()
|
||||||
|
if v, ok := values[SettingKeyRewriteMessageCacheControl]; ok && v != "" {
|
||||||
|
rewriteMessageCacheControl = v == "true"
|
||||||
|
}
|
||||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
|
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
|
||||||
fingerprintUnification: fp,
|
fingerprintUnification: fp,
|
||||||
metadataPassthrough: mp,
|
metadataPassthrough: mp,
|
||||||
cchSigning: cch,
|
cchSigning: cch,
|
||||||
anthropicCacheTTL1hInjection: cacheTTL1h,
|
anthropicCacheTTL1hInjection: cacheTTL1h,
|
||||||
|
rewriteMessageCacheControl: rewriteMessageCacheControl,
|
||||||
expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
|
expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
|
||||||
})
|
})
|
||||||
return gatewayForwardingSettingsResult{fp: fp, mp: mp, cch: cch, cacheTTL1h: cacheTTL1h}, nil
|
return gatewayForwardingSettingsResult{
|
||||||
|
fp: fp,
|
||||||
|
mp: mp,
|
||||||
|
cch: cch,
|
||||||
|
cacheTTL1h: cacheTTL1h,
|
||||||
|
rewriteMessageCacheControl: rewriteMessageCacheControl,
|
||||||
|
}, nil
|
||||||
})
|
})
|
||||||
if r, ok := val.(gatewayForwardingSettingsResult); ok {
|
if r, ok := val.(gatewayForwardingSettingsResult); ok {
|
||||||
return r
|
return r
|
||||||
@@ -1894,6 +1916,11 @@ func (s *SettingService) IsAnthropicCacheTTL1hInjectionEnabled(ctx context.Conte
|
|||||||
return s.getGatewayForwardingSettingsCached(ctx).cacheTTL1h
|
return s.getGatewayForwardingSettingsCached(ctx).cacheTTL1h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRewriteMessageCacheControlEnabled 检查是否启用 messages cache_control 改写。
|
||||||
|
func (s *SettingService) IsRewriteMessageCacheControlEnabled(ctx context.Context) bool {
|
||||||
|
return s.getGatewayForwardingSettingsCached(ctx).rewriteMessageCacheControl
|
||||||
|
}
|
||||||
|
|
||||||
// IsEmailVerifyEnabled 检查是否开启邮件验证
|
// IsEmailVerifyEnabled 检查是否开启邮件验证
|
||||||
func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool {
|
func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool {
|
||||||
value, err := s.settingRepo.GetValue(ctx, SettingKeyEmailVerifyEnabled)
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyEmailVerifyEnabled)
|
||||||
@@ -2358,6 +2385,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|||||||
// 分组隔离(默认不允许未分组 Key 调度)
|
// 分组隔离(默认不允许未分组 Key 调度)
|
||||||
SettingKeyAllowUngroupedKeyScheduling: "false",
|
SettingKeyAllowUngroupedKeyScheduling: "false",
|
||||||
SettingKeyEnableAnthropicCacheTTL1hInjection: "false",
|
SettingKeyEnableAnthropicCacheTTL1hInjection: "false",
|
||||||
|
SettingKeyRewriteMessageCacheControl: strconv.FormatBool(s.defaultRewriteMessageCacheControl()),
|
||||||
SettingPaymentVisibleMethodAlipaySource: "",
|
SettingPaymentVisibleMethodAlipaySource: "",
|
||||||
SettingPaymentVisibleMethodWxpaySource: "",
|
SettingPaymentVisibleMethodWxpaySource: "",
|
||||||
SettingPaymentVisibleMethodAlipayEnabled: "false",
|
SettingPaymentVisibleMethodAlipayEnabled: "false",
|
||||||
@@ -2734,6 +2762,11 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
result.EnableMetadataPassthrough = settings[SettingKeyEnableMetadataPassthrough] == "true"
|
result.EnableMetadataPassthrough = settings[SettingKeyEnableMetadataPassthrough] == "true"
|
||||||
result.EnableCCHSigning = settings[SettingKeyEnableCCHSigning] == "true"
|
result.EnableCCHSigning = settings[SettingKeyEnableCCHSigning] == "true"
|
||||||
result.EnableAnthropicCacheTTL1hInjection = settings[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
|
result.EnableAnthropicCacheTTL1hInjection = settings[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
|
||||||
|
if v, ok := settings[SettingKeyRewriteMessageCacheControl]; ok && v != "" {
|
||||||
|
result.RewriteMessageCacheControl = v == "true"
|
||||||
|
} else {
|
||||||
|
result.RewriteMessageCacheControl = s.defaultRewriteMessageCacheControl()
|
||||||
|
}
|
||||||
|
|
||||||
// Web search emulation: quick enabled check from the JSON config
|
// Web search emulation: quick enabled check from the JSON config
|
||||||
if raw := settings[SettingKeyWebSearchEmulationConfig]; raw != "" {
|
if raw := settings[SettingKeyWebSearchEmulationConfig]; raw != "" {
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ type SystemSettings struct {
|
|||||||
EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false)
|
EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false)
|
||||||
EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false)
|
EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false)
|
||||||
EnableAnthropicCacheTTL1hInjection bool // 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
|
EnableAnthropicCacheTTL1hInjection bool // 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
|
||||||
|
RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false)
|
||||||
|
|
||||||
// Web Search Emulation
|
// Web Search Emulation
|
||||||
WebSearchEmulationEnabled bool // 是否启用 web search 模拟
|
WebSearchEmulationEnabled bool // 是否启用 web search 模拟
|
||||||
|
|||||||
@@ -477,6 +477,7 @@ export interface SystemSettings {
|
|||||||
enable_metadata_passthrough: boolean;
|
enable_metadata_passthrough: boolean;
|
||||||
enable_cch_signing: boolean;
|
enable_cch_signing: boolean;
|
||||||
enable_anthropic_cache_ttl_1h_injection: boolean;
|
enable_anthropic_cache_ttl_1h_injection: boolean;
|
||||||
|
rewrite_message_cache_control: boolean;
|
||||||
web_search_emulation_enabled?: boolean;
|
web_search_emulation_enabled?: boolean;
|
||||||
|
|
||||||
// Payment configuration
|
// Payment configuration
|
||||||
@@ -673,6 +674,7 @@ export interface UpdateSettingsRequest {
|
|||||||
enable_metadata_passthrough?: boolean;
|
enable_metadata_passthrough?: boolean;
|
||||||
enable_cch_signing?: boolean;
|
enable_cch_signing?: boolean;
|
||||||
enable_anthropic_cache_ttl_1h_injection?: boolean;
|
enable_anthropic_cache_ttl_1h_injection?: boolean;
|
||||||
|
rewrite_message_cache_control?: boolean;
|
||||||
// Payment configuration
|
// Payment configuration
|
||||||
payment_enabled?: boolean;
|
payment_enabled?: boolean;
|
||||||
risk_control_enabled?: boolean;
|
risk_control_enabled?: boolean;
|
||||||
|
|||||||
@@ -5335,6 +5335,8 @@ export default {
|
|||||||
cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.',
|
cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.',
|
||||||
anthropicCacheTTL1hInjection: 'Anthropic Cache TTL Injection',
|
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.',
|
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',
|
||||||
|
rewriteMessageCacheControlHint: 'Default off: preserve client cache_control on message content blocks. When enabled, client breakpoints are stripped and proxy breakpoints are injected for clients that do not manage caching themselves.',
|
||||||
},
|
},
|
||||||
webSearchEmulation: {
|
webSearchEmulation: {
|
||||||
title: 'Web Search Emulation',
|
title: 'Web Search Emulation',
|
||||||
|
|||||||
@@ -5494,6 +5494,8 @@ export default {
|
|||||||
cchSigningHint: '对转发请求的 billing header 进行 CCH 哈希签名。关闭时保留原始占位符。',
|
cchSigningHint: '对转发请求的 billing header 进行 CCH 哈希签名。关闭时保留原始占位符。',
|
||||||
anthropicCacheTTL1hInjection: 'Anthropic 缓存 TTL 注入',
|
anthropicCacheTTL1hInjection: 'Anthropic 缓存 TTL 注入',
|
||||||
anthropicCacheTTL1hInjectionHint: '开启后,对 Anthropic OAuth/Setup Token 请求体中已有的 ephemeral 缓存块强制写入 1h;响应 usage 默认按 5m 回写计费,账号级 TTL 计费设置优先。',
|
anthropicCacheTTL1hInjectionHint: '开启后,对 Anthropic OAuth/Setup Token 请求体中已有的 ephemeral 缓存块强制写入 1h;响应 usage 默认按 5m 回写计费,账号级 TTL 计费设置优先。',
|
||||||
|
rewriteMessageCacheControl: '改写消息缓存断点',
|
||||||
|
rewriteMessageCacheControlHint: '默认关闭,保留客户端在 messages 内容块中的 cache_control。开启后会清除客户端断点并注入代理断点,适合不自行管理缓存策略的客户端。',
|
||||||
},
|
},
|
||||||
webSearchEmulation: {
|
webSearchEmulation: {
|
||||||
title: 'Web Search 模拟',
|
title: 'Web Search 模拟',
|
||||||
|
|||||||
@@ -3428,6 +3428,29 @@
|
|||||||
v-model="form.enable_anthropic_cache_ttl_1h_injection"
|
v-model="form.enable_anthropic_cache_ttl_1h_injection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- messages cache_control 改写 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"admin.settings.gatewayForwarding.rewriteMessageCacheControl",
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
"admin.settings.gatewayForwarding.rewriteMessageCacheControlHint",
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="form.rewrite_message_cache_control" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Web Search Emulation -->
|
<!-- Web Search Emulation -->
|
||||||
@@ -6547,6 +6570,7 @@ const form = reactive<SettingsForm>({
|
|||||||
enable_metadata_passthrough: false,
|
enable_metadata_passthrough: false,
|
||||||
enable_cch_signing: false,
|
enable_cch_signing: false,
|
||||||
enable_anthropic_cache_ttl_1h_injection: false,
|
enable_anthropic_cache_ttl_1h_injection: false,
|
||||||
|
rewrite_message_cache_control: false,
|
||||||
// Balance & quota notification
|
// Balance & quota notification
|
||||||
balance_low_notify_enabled: false,
|
balance_low_notify_enabled: false,
|
||||||
balance_low_notify_threshold: 0,
|
balance_low_notify_threshold: 0,
|
||||||
@@ -7617,6 +7641,7 @@ async function saveSettings() {
|
|||||||
enable_cch_signing: form.enable_cch_signing,
|
enable_cch_signing: form.enable_cch_signing,
|
||||||
enable_anthropic_cache_ttl_1h_injection:
|
enable_anthropic_cache_ttl_1h_injection:
|
||||||
form.enable_anthropic_cache_ttl_1h_injection,
|
form.enable_anthropic_cache_ttl_1h_injection,
|
||||||
|
rewrite_message_cache_control: form.rewrite_message_cache_control,
|
||||||
// Payment configuration
|
// Payment configuration
|
||||||
payment_enabled: form.payment_enabled,
|
payment_enabled: form.payment_enabled,
|
||||||
risk_control_enabled: form.risk_control_enabled,
|
risk_control_enabled: form.risk_control_enabled,
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const baseSettingsResponse = {
|
|||||||
enable_metadata_passthrough: false,
|
enable_metadata_passthrough: false,
|
||||||
enable_cch_signing: false,
|
enable_cch_signing: false,
|
||||||
enable_anthropic_cache_ttl_1h_injection: false,
|
enable_anthropic_cache_ttl_1h_injection: false,
|
||||||
|
rewrite_message_cache_control: false,
|
||||||
payment_enabled: true,
|
payment_enabled: true,
|
||||||
payment_min_amount: 1,
|
payment_min_amount: 1,
|
||||||
payment_max_amount: 10000,
|
payment_max_amount: 10000,
|
||||||
@@ -601,6 +602,26 @@ describe("admin SettingsView payment visible method controls", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("submits message cache_control rewrite gateway setting", async () => {
|
||||||
|
getSettings.mockResolvedValueOnce({
|
||||||
|
...baseSettingsResponse,
|
||||||
|
rewrite_message_cache_control: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mountView();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await wrapper.find("form").trigger("submit.prevent");
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(updateSettings).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updateSettings).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
rewrite_message_cache_control: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("updates provider enablement immediately and reloads providers", async () => {
|
it("updates provider enablement immediately and reloads providers", async () => {
|
||||||
const provider = {
|
const provider = {
|
||||||
id: 7,
|
id: 7,
|
||||||
|
|||||||
Reference in New Issue
Block a user