Merge pull request #7339 from superman2003/fix/openai-stream-error-status-field

fix(openai): honor error.status when classifying in-stream failures
This commit is contained in:
Wesley Liddick
2026-09-22 20:30:08 +08:00
committed by GitHub
2 changed files with 75 additions and 2 deletions
@@ -226,6 +226,66 @@ func TestOpenAIStreamSemanticStatusesPreservedAcrossTerminalShapes(t *testing.T)
}
}
// 不少 OpenAI 兼容上游在流内错误对象里用 status 而不是 status_code 报告状态码。
// 只认 status_code 会把它们降级成通用 502,账号健康与 failover 判定随之失效。
func TestOpenAIStreamSemanticStatusHonorsErrorStatusAlias(t *testing.T) {
tests := []struct {
name string
body string
status int
wantFailover bool
}{
{
name: "response.error.status unauthorized",
body: `{"type":"response.failed","response":{"error":{"code":"server_error","status":401,"message":"upstream rejected the key"}}}`,
status: http.StatusUnauthorized,
wantFailover: true,
},
{
name: "error.status rate limited",
body: `{"type":"error","error":{"code":"server_error","status":429,"message":"upstream is busy"}}`,
status: http.StatusTooManyRequests,
wantFailover: true,
},
{
name: "error.status overloaded",
body: `{"type":"error","error":{"code":"server_error","status":529,"message":"upstream is overloaded"}}`,
status: 529,
wantFailover: true,
},
{
name: "response.error.status forbidden without account signal",
body: `{"type":"response.failed","response":{"error":{"code":"server_error","status":403,"message":"request was rejected"}}}`,
status: http.StatusForbidden,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
payload := []byte(tt.body)
message := extractOpenAISSEErrorMessage(payload)
require.Equal(t, tt.status, openAIStreamFailureStatus(payload, message))
require.Equal(t, tt.wantFailover, openAIStreamErrorEventShouldFailover(payload, message))
require.Equal(t, tt.wantFailover, openAIStreamFailedEventShouldFailover(payload, message))
})
}
}
func TestOpenAIStreamCredentialFailureHonorsErrorStatusAlias(t *testing.T) {
require.True(t, openAIStreamCredentialAuthFailure([]byte(`{"type":"error","error":{"code":"server_error","status":401,"message":"credential rejected"}}`)))
require.True(t, openAIStreamCredentialAuthFailure([]byte(`{"type":"response.failed","response":{"error":{"code":"server_error","status":401,"message":"credential rejected"}}}`)))
require.False(t, openAIStreamCredentialAuthFailure([]byte(`{"type":"response.failed","response":{"error":{"code":"server_error","status":500,"message":"transient upstream failure"}}}`)))
}
// 上游把 status 报成 5xx 时仍然是通用上游故障:既不改账号状态,也不冒充认证/限流。
func TestOpenAIStreamErrorStatusAliasKeepsGeneric5xxUnclassified(t *testing.T) {
payload := []byte(`{"type":"response.failed","response":{"error":{"code":"server_error","status":500,"type":"server_error","message":"rv_shape_invalid: temporary response validation failure after partial output; retry your request if your client can recover partial output."},"id":"resp_0","object":"response","status":"failed"},"sequence_number":80}`)
message := extractOpenAISSEErrorMessage(payload)
require.Equal(t, http.StatusBadGateway, openAIStreamFailureStatus(payload, message))
require.False(t, openAIStreamCredentialAuthFailure(payload))
require.True(t, openAIStreamFailedEventShouldFailover(payload, message))
}
func TestOpenAIStreamBareErrorUsesSemanticFailover(t *testing.T) {
payload := []byte(`{"error":{"type":"rate_limit_error","code":"rate_limit_exceeded","message":"slow down"}}`)
require.True(t, openAIStreamErrorEventShouldFailover(payload, "slow down"))
@@ -1366,6 +1366,19 @@ func sanitizeOpenAICapacityShedErrorCodeForClient(payload []byte) ([]byte, bool)
return updated, changed
}
// openAIStreamErrorStatusPaths 覆盖流内 error / response.failed 事件里上游状态码的
// 两种拼写:OpenAI 用 status_code,而不少 OpenAI 兼容上游(含二级中转)只写 status。
// 只认 status_code 会把 401/403/429/529 一律降级成通用 502,账号健康与 failover
// 判定随之失效。WS 路径的 openAIWSPayloadTransientStatus 早已同时读两种拼写。
var openAIStreamErrorStatusPaths = []string{
"response.error.status_code",
"response.error.status",
"error.status_code",
"error.status",
"status_code",
"status",
}
func openAIStreamFailedEventSemanticStatus(payload []byte, message string) int {
if isOpenAIContextWindowError(message, payload) {
return http.StatusBadRequest
@@ -1377,7 +1390,7 @@ func openAIStreamFailedEventSemanticStatus(payload []byte, message string) int {
errType = strings.ToLower(strings.TrimSpace(gjson.GetBytes(payload, "error.type").String()))
}
combined := strings.TrimSpace(errType + " " + code + " " + strings.ToLower(strings.TrimSpace(message)))
for _, path := range []string{"response.error.status_code", "error.status_code", "status_code"} {
for _, path := range openAIStreamErrorStatusPaths {
if status := int(gjson.GetBytes(payload, path).Int()); status == http.StatusUnauthorized ||
status == http.StatusForbidden || status == http.StatusTooManyRequests || status == 529 {
return status
@@ -1425,7 +1438,7 @@ func openAIStreamCredentialAuthFailure(payload []byte) bool {
if len(bytes.TrimSpace(payload)) == 0 || !gjson.ValidBytes(payload) {
return false
}
for _, path := range []string{"response.error.status_code", "error.status_code", "status_code"} {
for _, path := range openAIStreamErrorStatusPaths {
if int(gjson.GetBytes(payload, path).Int()) == http.StatusUnauthorized {
return true
}