mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(openai): 跨组会话失配保护移到生效的 WSv2 路径并补测
87dd5f5d 把 previous_response_id 剥离保护加在了 HTTP Responses 路径,但该路径
在 previousResponseID != "" 时已无条件返回 400(0fcddce6 引入),剥离块恒不可达,
RemovePreviousResponseIDFromBody 也只被这段死分支调用、无覆盖。
- 删除 HTTP Responses 路径的死代码剥离块,留注释指明保护应在 WSv2 路径。
- 在 ResponsesWebSocket 首包(wsFirstMessage)处补回等价保护:previous_response_id
未命中当前分组粘连账号(StickyPreviousHit=false)时剥离,改用首包 input 重建;
带 function_call_output 的工具续链保持原样。这是 previous_response_id 真正生效、
会触发跨组会话链鉴权失败的路径。
- 为 RemovePreviousResponseIDFromBody 增加单元测试(现已有真实 caller)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -358,17 +358,6 @@ 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() {
|
||||
@@ -1504,6 +1493,18 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
|
||||
if channelMappingWS.Mapped {
|
||||
wsFirstMessage = h.gatewayService.ReplaceModelInBody(firstMessage, channelMappingWS.MappedModel)
|
||||
}
|
||||
// 切组/会话失配防护:previous_response_id 未在当前分组命中粘连账号(StickyPreviousHit=false),
|
||||
// 说明该会话链不属于本次调度到的账号,原样转发会触发上游会话链鉴权失败(“鉴权失败,请检查 API Key”)。
|
||||
// 故剥离首包里的 previous_response_id,改用首包内 input 重建上下文;带 function_call_output 的
|
||||
// 工具续链无法重建,保持原样。仅作用于首轮首包,后续 turn 的续链由 WS 转发层既有逻辑处理。
|
||||
if previousResponseID != "" && !scheduleDecision.StickyPreviousHit &&
|
||||
!service.ValidateFunctionCallOutputContextBytes(wsFirstMessage).HasFunctionCallOutput {
|
||||
wsFirstMessage = service.RemovePreviousResponseIDFromBody(wsFirstMessage)
|
||||
reqLog.Debug("openai.websocket_previous_response_id_stripped_cross_group",
|
||||
zap.Int64("account_id", account.ID),
|
||||
zap.String("schedule_layer", scheduleDecision.Layer),
|
||||
)
|
||||
}
|
||||
|
||||
// WebSocket 首包可能很大,hash 必须在 hooks 外算成字符串,避免 AfterTurn 闭包保活请求体。
|
||||
requestPayloadHash = service.HashUsageRequestPayload(wsFirstMessage)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1921,6 +1922,33 @@ func TestReplaceModelInBody_InvalidJSON(t *testing.T) {
|
||||
require.Equal(t, arrayBody, result2)
|
||||
}
|
||||
|
||||
func TestRemovePreviousResponseIDFromBody(t *testing.T) {
|
||||
t.Run("empty body returned as-is", func(t *testing.T) {
|
||||
require.Equal(t, []byte{}, RemovePreviousResponseIDFromBody([]byte{}))
|
||||
require.Nil(t, RemovePreviousResponseIDFromBody(nil))
|
||||
})
|
||||
|
||||
t.Run("no previous_response_id field is a no-op", func(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5","input":"hi"}`)
|
||||
result := RemovePreviousResponseIDFromBody(body)
|
||||
require.Equal(t, body, result)
|
||||
})
|
||||
|
||||
t.Run("strips previous_response_id and preserves other fields", func(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5","previous_response_id":"resp_abc","input":"hi"}`)
|
||||
result := RemovePreviousResponseIDFromBody(body)
|
||||
require.False(t, gjson.GetBytes(result, "previous_response_id").Exists())
|
||||
require.Equal(t, "gpt-5", gjson.GetBytes(result, "model").String())
|
||||
require.Equal(t, "hi", gjson.GetBytes(result, "input").String())
|
||||
})
|
||||
|
||||
t.Run("empty-string previous_response_id is also stripped", func(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5","previous_response_id":""}`)
|
||||
result := RemovePreviousResponseIDFromBody(body)
|
||||
require.False(t, gjson.GetBytes(result, "previous_response_id").Exists())
|
||||
})
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 7. isPlatformPricingMatch
|
||||
// ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user