feat(rectifier): 请求整流器增加 API Key 账号签名整流支持

新增独立开关控制 API Key 账号的签名整流功能,支持配置自定义
匹配关键词以捕获不同格式的上游错误响应。

- 新增 apikey_signature_enabled 开关(默认关闭)
- 新增 apikey_signature_patterns 自定义关键词配置
- 内置签名检测规则对 API Key 账号同样生效
- 自定义关键词对完整响应体做不区分大小写匹配
- 重试二阶段检测仅做模式匹配,不重复校验开关
- Handler 层校验关键词数量(≤50)和长度(≤500)
- API 响应 nil patterns 统一序列化为空数组
- OAuth/SetupToken/Upstream/Bedrock 账号行为不变
This commit is contained in:
shaw
2026-03-26 16:43:38 +08:00
parent ce96527dd9
commit d571f300e5
8 changed files with 204 additions and 14 deletions

View File

@@ -1594,18 +1594,26 @@ func (h *SettingHandler) GetRectifierSettings(c *gin.Context) {
return
}
patterns := settings.APIKeySignaturePatterns
if patterns == nil {
patterns = []string{}
}
response.Success(c, dto.RectifierSettings{
Enabled: settings.Enabled,
ThinkingSignatureEnabled: settings.ThinkingSignatureEnabled,
ThinkingBudgetEnabled: settings.ThinkingBudgetEnabled,
APIKeySignatureEnabled: settings.APIKeySignatureEnabled,
APIKeySignaturePatterns: patterns,
})
}
// UpdateRectifierSettingsRequest 更新整流器配置请求
type UpdateRectifierSettingsRequest struct {
Enabled bool `json:"enabled"`
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
Enabled bool `json:"enabled"`
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
APIKeySignatureEnabled bool `json:"apikey_signature_enabled"`
APIKeySignaturePatterns []string `json:"apikey_signature_patterns"`
}
// UpdateRectifierSettings 更新请求整流器配置
@@ -1617,10 +1625,32 @@ func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) {
return
}
// 校验并清理自定义匹配关键词
const maxPatterns = 50
const maxPatternLen = 500
if len(req.APIKeySignaturePatterns) > maxPatterns {
response.BadRequest(c, "Too many signature patterns (max 50)")
return
}
var cleanedPatterns []string
for _, p := range req.APIKeySignaturePatterns {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if len(p) > maxPatternLen {
response.BadRequest(c, "Signature pattern too long (max 500 characters)")
return
}
cleanedPatterns = append(cleanedPatterns, p)
}
settings := &service.RectifierSettings{
Enabled: req.Enabled,
ThinkingSignatureEnabled: req.ThinkingSignatureEnabled,
ThinkingBudgetEnabled: req.ThinkingBudgetEnabled,
APIKeySignatureEnabled: req.APIKeySignatureEnabled,
APIKeySignaturePatterns: cleanedPatterns,
}
if err := h.settingService.SetRectifierSettings(c.Request.Context(), settings); err != nil {
@@ -1635,10 +1665,16 @@ func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) {
return
}
updatedPatterns := updatedSettings.APIKeySignaturePatterns
if updatedPatterns == nil {
updatedPatterns = []string{}
}
response.Success(c, dto.RectifierSettings{
Enabled: updatedSettings.Enabled,
ThinkingSignatureEnabled: updatedSettings.ThinkingSignatureEnabled,
ThinkingBudgetEnabled: updatedSettings.ThinkingBudgetEnabled,
APIKeySignatureEnabled: updatedSettings.APIKeySignatureEnabled,
APIKeySignaturePatterns: updatedPatterns,
})
}

View File

@@ -188,9 +188,11 @@ type StreamTimeoutSettings struct {
// RectifierSettings 请求整流器配置 DTO
type RectifierSettings struct {
Enabled bool `json:"enabled"`
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
Enabled bool `json:"enabled"`
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
APIKeySignatureEnabled bool `json:"apikey_signature_enabled"`
APIKeySignaturePatterns []string `json:"apikey_signature_patterns"`
}
// BetaPolicyRule Beta 策略规则 DTO

View File

@@ -4188,7 +4188,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
if readErr == nil {
_ = resp.Body.Close()
if s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) {
if s.shouldRectifySignatureError(ctx, account, respBody) {
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
Platform: account.Platform,
AccountID: account.ID,
@@ -4243,7 +4243,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
retryRespBody, retryReadErr := io.ReadAll(io.LimitReader(retryResp.Body, 2<<20))
_ = retryResp.Body.Close()
if retryReadErr == nil && retryResp.StatusCode == 400 && s.isThinkingBlockSignatureError(retryRespBody) {
if retryReadErr == nil && retryResp.StatusCode == 400 && s.isSignatureErrorPattern(ctx, account, retryRespBody) {
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
Platform: account.Platform,
AccountID: account.ID,
@@ -6145,6 +6145,59 @@ func truncateForLog(b []byte, maxBytes int) string {
return s
}
// shouldRectifySignatureError 统一判断是否应触发签名整流strip thinking blocks 并重试)。
// 根据账号类型检查对应的开关和匹配模式。
func (s *GatewayService) shouldRectifySignatureError(ctx context.Context, account *Account, respBody []byte) bool {
if account.Type == AccountTypeAPIKey {
// API Key 账号:独立开关,一次读取配置
settings, err := s.settingService.GetRectifierSettings(ctx)
if err != nil || !settings.Enabled || !settings.APIKeySignatureEnabled {
return false
}
// 先检查内置模式(同 OAuth再检查自定义关键词
if s.isThinkingBlockSignatureError(respBody) {
return true
}
return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns)
}
// OAuth/SetupToken/Upstream/Bedrock 等:保持原有行为(内置模式 + 原开关)
return s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx)
}
// isSignatureErrorPattern 仅做模式匹配,不检查开关。
// 用于已进入重试流程后的二阶段检测(此时开关已在首次调用时验证过)。
func (s *GatewayService) isSignatureErrorPattern(ctx context.Context, account *Account, respBody []byte) bool {
if s.isThinkingBlockSignatureError(respBody) {
return true
}
if account.Type == AccountTypeAPIKey {
settings, err := s.settingService.GetRectifierSettings(ctx)
if err != nil {
return false
}
return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns)
}
return false
}
// matchSignaturePatterns 检查响应体是否匹配自定义关键词列表(不区分大小写)。
func matchSignaturePatterns(respBody []byte, patterns []string) bool {
if len(patterns) == 0 {
return false
}
bodyLower := strings.ToLower(string(respBody))
for _, p := range patterns {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if strings.Contains(bodyLower, strings.ToLower(p)) {
return true
}
}
return false
}
// isThinkingBlockSignatureError 检测是否是thinking block相关错误
// 这类错误可以通过过滤thinking blocks并重试来解决
func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool {
@@ -8013,7 +8066,7 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
}
// 检测 thinking block 签名错误400并重试一次过滤 thinking blocks
if resp.StatusCode == 400 && s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx) {
if resp.StatusCode == 400 && s.shouldRectifySignatureError(ctx, account, respBody) {
logger.LegacyPrintf("service.gateway", "Account %d: detected thinking block signature error on count_tokens, retrying with filtered thinking blocks", account.ID)
filteredBody := FilterThinkingBlocksForRetry(body)

View File

@@ -190,9 +190,11 @@ func DefaultStreamTimeoutSettings() *StreamTimeoutSettings {
// RectifierSettings 请求整流器配置
type RectifierSettings struct {
Enabled bool `json:"enabled"` // 总开关
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` // Thinking 签名整流
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` // Thinking Budget 整流
Enabled bool `json:"enabled"` // 总开关
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` // Thinking 签名整流
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"` // Thinking Budget 整流
APIKeySignatureEnabled bool `json:"apikey_signature_enabled"` // API Key 签名整流开关
APIKeySignaturePatterns []string `json:"apikey_signature_patterns"` // API Key 自定义匹配关键词
}
// DefaultRectifierSettings 返回默认的整流器配置(全部启用)