fix: sanitize verbose OpenAI response failed events

This commit is contained in:
lyen1688
2026-06-26 12:29:58 +08:00
parent 683a8d8096
commit 8a7269f539
2 changed files with 137 additions and 0 deletions
@@ -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{