mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-22 06:40:21 +08:00
fix: sanitize verbose OpenAI response failed events
This commit is contained in:
@@ -3933,6 +3933,11 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
|
||||
responseID = extractOpenAIResponseIDFromJSONBytes(dataBytes)
|
||||
}
|
||||
imageCounter.AddSSEData(dataBytes)
|
||||
if sanitizedData, sanitized := sanitizeOpenAIResponseFailedEventForClient(dataBytes, eventType); sanitized {
|
||||
dataBytes = sanitizedData
|
||||
trimmedData = strings.TrimSpace(string(sanitizedData))
|
||||
line = "data: " + string(sanitizedData)
|
||||
}
|
||||
lineStartsClientOutput = forceFlushFailedEvent || openAIStreamDataStartsClientOutput(trimmedData, eventType)
|
||||
if firstTokenMs == nil && lineStartsClientOutput && trimmedData != "[DONE]" {
|
||||
ms := int(time.Since(startTime).Milliseconds())
|
||||
@@ -4895,6 +4900,11 @@ func (s *OpenAIGatewayService) handleStreamingResponse(ctx context.Context, resp
|
||||
line = "data: " + data
|
||||
eventType = strings.TrimSpace(gjson.GetBytes(dataBytes, "type").String())
|
||||
}
|
||||
if sanitizedData, sanitized := sanitizeOpenAIResponseFailedEventForClient(dataBytes, eventType); sanitized {
|
||||
dataBytes = sanitizedData
|
||||
data = string(sanitizedData)
|
||||
line = "data: " + data
|
||||
}
|
||||
// Replace model in response if needed.
|
||||
// Fast path: most events do not contain model field values.
|
||||
if needModelReplace && mappedModel != "" && strings.Contains(line, mappedModel) {
|
||||
@@ -5509,6 +5519,37 @@ func extractOpenAISSEErrorMessage(payload []byte) string {
|
||||
return sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(payload)))
|
||||
}
|
||||
|
||||
func sanitizeOpenAIResponseFailedEventForClient(payload []byte, eventType string) ([]byte, bool) {
|
||||
if eventType != "response.failed" || len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||
return payload, false
|
||||
}
|
||||
if !gjson.GetBytes(payload, "response").Exists() {
|
||||
return payload, false
|
||||
}
|
||||
updated := payload
|
||||
for _, path := range []string{
|
||||
"response.instructions",
|
||||
"response.output",
|
||||
"response.usage",
|
||||
"response.metadata",
|
||||
"response.reasoning",
|
||||
"response.tools",
|
||||
"response.tool_choice",
|
||||
"response.parallel_tool_calls",
|
||||
"response.text",
|
||||
"response.truncation",
|
||||
"response.max_output_tokens",
|
||||
"response.incomplete_details",
|
||||
} {
|
||||
next, err := sjson.DeleteBytes(updated, path)
|
||||
if err != nil {
|
||||
return payload, false
|
||||
}
|
||||
updated = next
|
||||
}
|
||||
return updated, !bytes.Equal(updated, payload)
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) writeOpenAINonStreamingProtocolError(resp *http.Response, c *gin.Context, message string) error {
|
||||
message = sanitizeUpstreamErrorMessage(strings.TrimSpace(message))
|
||||
if message == "" {
|
||||
|
||||
@@ -1368,6 +1368,55 @@ func TestOpenAIStreamingResponseFailedBeforeOutputServerOverloadedCodeReturnsFai
|
||||
require.Empty(t, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestOpenAIStreamingResponseFailedAfterOutputSanitizesVerboseResponseForClient(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
cfg := &config.Config{
|
||||
Gateway: config.GatewayConfig{
|
||||
StreamDataIntervalTimeout: 0,
|
||||
StreamKeepaliveInterval: 0,
|
||||
MaxLineSize: defaultMaxLineSize,
|
||||
},
|
||||
}
|
||||
svc := &OpenAIGatewayService{cfg: cfg}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
longInstructions := strings.Repeat("You are GPT-5.1 running in the Codex CLI. ", 20)
|
||||
failedPayload := fmt.Sprintf(
|
||||
`{"type":"response.failed","response":{"id":"resp_failed","object":"response","created_at":1782446336,"status":"failed","instructions":%q,"output":[{"type":"message","content":[{"type":"output_text","text":"large"}]}],"usage":{"input_tokens":123,"output_tokens":0},"error":{"code":"context_length_exceeded","message":"Your input exceeds the context window of this model. Please adjust your input and try again."}}}`,
|
||||
longInstructions,
|
||||
)
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(strings.Join([]string{
|
||||
"event: response.created",
|
||||
`data: {"type":"response.created","response":{"id":"resp_failed"}}`,
|
||||
"",
|
||||
"event: response.output_text.delta",
|
||||
`data: {"type":"response.output_text.delta","delta":"partial"}`,
|
||||
"",
|
||||
"event: response.failed",
|
||||
"data: " + failedPayload,
|
||||
"",
|
||||
}, "\n"))),
|
||||
Header: http.Header{"X-Request-Id": []string{"rid-failed-after-output"}},
|
||||
}
|
||||
|
||||
_, err := svc.handleStreamingResponse(c.Request.Context(), resp, c, &Account{ID: 1, Platform: PlatformOpenAI, Name: "acc"}, time.Now(), "model", "model")
|
||||
require.Error(t, err)
|
||||
|
||||
body := rec.Body.String()
|
||||
require.Contains(t, body, "event: response.failed")
|
||||
require.Contains(t, body, "context_length_exceeded")
|
||||
require.Contains(t, body, "Your input exceeds the context window")
|
||||
require.NotContains(t, body, "You are GPT-5.1 running in the Codex CLI")
|
||||
require.NotContains(t, body, `"instructions"`)
|
||||
require.NotContains(t, body, `"output"`)
|
||||
require.NotContains(t, body, `"usage"`)
|
||||
}
|
||||
|
||||
func TestOpenAIStreamingPreambleOnlyMissingTerminalReturnsFailover(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
cfg := &config.Config{
|
||||
@@ -1707,6 +1756,53 @@ func TestOpenAIStreamingPassthroughResponseFailedBeforeOutputReturnsFailover(t *
|
||||
require.Empty(t, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestOpenAIStreamingPassthroughResponseFailedAfterOutputSanitizesVerboseResponseForClient(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
cfg := &config.Config{
|
||||
Gateway: config.GatewayConfig{
|
||||
MaxLineSize: defaultMaxLineSize,
|
||||
},
|
||||
}
|
||||
svc := &OpenAIGatewayService{cfg: cfg}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
longInstructions := strings.Repeat("You are GPT-5.1 running in the Codex CLI. ", 20)
|
||||
failedPayload := fmt.Sprintf(
|
||||
`{"type":"response.failed","response":{"id":"resp_pass_failed","object":"response","created_at":1782446336,"status":"failed","instructions":%q,"output":[{"type":"message","content":[{"type":"output_text","text":"large"}]}],"usage":{"input_tokens":123,"output_tokens":0},"error":{"code":"context_length_exceeded","message":"Your input exceeds the context window of this model. Please adjust your input and try again."}}}`,
|
||||
longInstructions,
|
||||
)
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(strings.Join([]string{
|
||||
"event: response.created",
|
||||
`data: {"type":"response.created","response":{"id":"resp_pass_failed"}}`,
|
||||
"",
|
||||
"event: response.output_text.delta",
|
||||
`data: {"type":"response.output_text.delta","delta":"partial"}`,
|
||||
"",
|
||||
"event: response.failed",
|
||||
"data: " + failedPayload,
|
||||
"",
|
||||
}, "\n"))),
|
||||
Header: http.Header{"X-Request-Id": []string{"rid-pass-failed-after-output"}},
|
||||
}
|
||||
|
||||
_, err := svc.handleStreamingResponsePassthrough(c.Request.Context(), resp, c, &Account{ID: 1, Platform: PlatformOpenAI, Name: "acc"}, time.Now(), "", "")
|
||||
require.Error(t, err)
|
||||
|
||||
body := rec.Body.String()
|
||||
require.Contains(t, body, "event: response.failed")
|
||||
require.Contains(t, body, "context_length_exceeded")
|
||||
require.Contains(t, body, "Your input exceeds the context window")
|
||||
require.NotContains(t, body, "You are GPT-5.1 running in the Codex CLI")
|
||||
require.NotContains(t, body, `"instructions"`)
|
||||
require.NotContains(t, body, `"output"`)
|
||||
require.NotContains(t, body, `"usage"`)
|
||||
}
|
||||
|
||||
func TestOpenAIStreamingPassthroughResponseDoneWithoutDoneMarkerStillSucceeds(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
cfg := &config.Config{
|
||||
|
||||
Reference in New Issue
Block a user