mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(openai): 切组后剥离失配的 previous_response_id,修复跨组会话鉴权失败
用户从公开组切回订阅组后,客户端沿用旧 Responses 会话 ID,订阅组调度到
不拥有该会话链的账号,上游因会话链鉴权不匹配返回“鉴权失败,请检查 API Key”。
两处修复:
- openai_ws_state_store: 本地热缓存 responseToAccount 改为按 {groupID}:{responseID}
命名空间,与 Redis 层 sticky_session:{groupID}:... 一致,避免单实例下跨组命中
其他分组遗留的本地绑定,确保 StickyPreviousHit 信号可信。
- openai_gateway_handler: 转发前若 previous_response_id 未在当前分组命中粘连账号
(StickyPreviousHit=false),主动剥离并改用完整 input 重建上下文;带
function_call_output 的工具续链保持原样(与 WS 重连恢复逻辑一致)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -358,6 +358,17 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
if channelMapping.Mapped {
|
||||
forwardBody = h.gatewayService.ReplaceModelInBody(body, channelMapping.MappedModel)
|
||||
}
|
||||
// 切组/会话失配防护:previous_response_id 未在当前分组命中粘连账号(StickyPreviousHit=false)
|
||||
// 说明会话链不属于本次账号,携带它会触发上游会话链鉴权失败,故主动剥离改用完整 input 重建。
|
||||
// 带 function_call_output 的工具续链无法重建,保持原样(与 WS 重连恢复一致)。
|
||||
if previousResponseID != "" && !scheduleDecision.StickyPreviousHit &&
|
||||
!service.ValidateFunctionCallOutputContextBytes(forwardBody).HasFunctionCallOutput {
|
||||
forwardBody = service.RemovePreviousResponseIDFromBody(forwardBody)
|
||||
reqLog.Debug("openai.previous_response_id_stripped_cross_group",
|
||||
zap.Int64("account_id", account.ID),
|
||||
zap.String("schedule_layer", scheduleDecision.Layer),
|
||||
)
|
||||
}
|
||||
writerSizeBeforeForward := c.Writer.Size()
|
||||
result, err := func() (*service.OpenAIForwardResult, error) {
|
||||
defer func() {
|
||||
|
||||
@@ -572,6 +572,21 @@ func ReplaceModelInBody(body []byte, newModel string) []byte {
|
||||
return newBody
|
||||
}
|
||||
|
||||
// RemovePreviousResponseIDFromBody 删除请求体中的 previous_response_id,用于会话失配时改用完整 input 重建上下文。
|
||||
func RemovePreviousResponseIDFromBody(body []byte) []byte {
|
||||
if len(body) == 0 {
|
||||
return body
|
||||
}
|
||||
if !gjson.GetBytes(body, "previous_response_id").Exists() {
|
||||
return body
|
||||
}
|
||||
newBody, err := sjson.DeleteBytes(body, "previous_response_id")
|
||||
if err != nil {
|
||||
return body
|
||||
}
|
||||
return newBody
|
||||
}
|
||||
|
||||
// validateChannelConfig 校验渠道的定价和映射配置(冲突检测 + 区间校验 + 计费模式校验)。
|
||||
// Create 和 Update 共用此函数,避免重复。
|
||||
func validateChannelConfig(pricing []ChannelModelPricing, mapping map[string]map[string]string) error {
|
||||
|
||||
@@ -100,9 +100,10 @@ func (s *defaultOpenAIWSStateStore) BindResponseAccount(ctx context.Context, gro
|
||||
s.maybeCleanup()
|
||||
|
||||
expiresAt := time.Now().Add(ttl)
|
||||
mapKey := openAIWSResponseAccountMapKey(groupID, id)
|
||||
s.responseToAccountMu.Lock()
|
||||
ensureBindingCapacity(s.responseToAccount, id, openAIWSStateStoreMaxEntriesPerMap)
|
||||
s.responseToAccount[id] = openAIWSAccountBinding{accountID: accountID, expiresAt: expiresAt}
|
||||
ensureBindingCapacity(s.responseToAccount, mapKey, openAIWSStateStoreMaxEntriesPerMap)
|
||||
s.responseToAccount[mapKey] = openAIWSAccountBinding{accountID: accountID, expiresAt: expiresAt}
|
||||
s.responseToAccountMu.Unlock()
|
||||
|
||||
if s.cache == nil {
|
||||
@@ -122,8 +123,9 @@ func (s *defaultOpenAIWSStateStore) GetResponseAccount(ctx context.Context, grou
|
||||
s.maybeCleanup()
|
||||
|
||||
now := time.Now()
|
||||
mapKey := openAIWSResponseAccountMapKey(groupID, id)
|
||||
s.responseToAccountMu.RLock()
|
||||
if binding, ok := s.responseToAccount[id]; ok {
|
||||
if binding, ok := s.responseToAccount[mapKey]; ok {
|
||||
if now.Before(binding.expiresAt) {
|
||||
accountID := binding.accountID
|
||||
s.responseToAccountMu.RUnlock()
|
||||
@@ -153,7 +155,7 @@ func (s *defaultOpenAIWSStateStore) DeleteResponseAccount(ctx context.Context, g
|
||||
return nil
|
||||
}
|
||||
s.responseToAccountMu.Lock()
|
||||
delete(s.responseToAccount, id)
|
||||
delete(s.responseToAccount, openAIWSResponseAccountMapKey(groupID, id))
|
||||
s.responseToAccountMu.Unlock()
|
||||
|
||||
if s.cache == nil {
|
||||
@@ -417,6 +419,11 @@ func openAIWSResponseAccountCacheKey(responseID string) string {
|
||||
return openAIWSResponseAccountCachePrefix + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// openAIWSResponseAccountMapKey 本地热缓存按分组隔离的 key,与 Redis 层保持一致,避免跨组命中。
|
||||
func openAIWSResponseAccountMapKey(groupID int64, responseID string) string {
|
||||
return fmt.Sprintf("%d:%s", groupID, responseID)
|
||||
}
|
||||
|
||||
func normalizeOpenAIWSTTL(ttl time.Duration) time.Duration {
|
||||
if ttl <= 0 {
|
||||
return time.Hour
|
||||
|
||||
Reference in New Issue
Block a user