mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 08:25:35 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user