mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
@@ -425,6 +425,21 @@ func (s *OpenAIGatewayService) handleChatBufferedStreamingResponse(
|
||||
return nil, s.newOpenAIStreamFailoverError(c, account, false, requestID, payload, message)
|
||||
}
|
||||
message = s.recordOpenAIStreamUpstreamError(c, account, false, requestID, "http_error", payload, message)
|
||||
// response.failed 到达在 HTTP 200 SSE 流上,无真实 HTTP 错误码,传 0。
|
||||
if status, errType, errMsg, matched := applyErrorPassthroughRule(
|
||||
c, account.Platform, 0, payload,
|
||||
http.StatusBadGateway, "upstream_error", message,
|
||||
); matched {
|
||||
if status == 0 {
|
||||
status = http.StatusBadGateway
|
||||
}
|
||||
if errMsg == "" {
|
||||
errMsg = message
|
||||
}
|
||||
MarkResponseCommitted(c)
|
||||
writeChatCompletionsError(c, status, errType, errMsg)
|
||||
return nil, fmt.Errorf("upstream response failed (passthrough): %s", errMsg)
|
||||
}
|
||||
writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", message)
|
||||
return nil, fmt.Errorf("upstream response failed: %s", message)
|
||||
}
|
||||
@@ -581,14 +596,28 @@ func (s *OpenAIGatewayService) handleChatStreamingResponse(
|
||||
return true
|
||||
}
|
||||
message = s.recordOpenAIStreamUpstreamError(c, account, false, requestID, "http_error", payloadBytes, message)
|
||||
defaultStatus, defaultErrType, defaultMsg := http.StatusBadGateway, "upstream_error", message
|
||||
if status, errType, errMsg, matched := applyErrorPassthroughRule(
|
||||
c, account.Platform, 0, payloadBytes,
|
||||
defaultStatus, defaultErrType, defaultMsg,
|
||||
); matched {
|
||||
if status == 0 {
|
||||
status = defaultStatus
|
||||
}
|
||||
if errMsg == "" {
|
||||
errMsg = defaultMsg
|
||||
}
|
||||
defaultStatus, defaultErrType, defaultMsg = status, errType, errMsg
|
||||
MarkResponseCommitted(c)
|
||||
}
|
||||
errorPayload, _ := json.Marshal(gin.H{
|
||||
"error": gin.H{
|
||||
"type": "upstream_error",
|
||||
"message": message,
|
||||
"type": defaultErrType,
|
||||
"message": defaultMsg,
|
||||
},
|
||||
})
|
||||
if c != nil && c.Writer != nil && !c.Writer.Written() {
|
||||
writeChatCompletionsError(c, http.StatusBadGateway, "upstream_error", message)
|
||||
writeChatCompletionsError(c, defaultStatus, defaultErrType, defaultMsg)
|
||||
clientOutputStarted = true
|
||||
} else if c != nil && c.Writer != nil && !clientDisconnected {
|
||||
if _, err := fmt.Fprintf(c.Writer, "data: %s\n\n", errorPayload); err != nil {
|
||||
|
||||
@@ -465,6 +465,20 @@ func (s *OpenAIGatewayService) handleAnthropicBufferedStreamingResponse(
|
||||
return nil, s.newOpenAIStreamFailoverError(c, account, false, requestID, payload, message)
|
||||
}
|
||||
message = s.recordOpenAIStreamUpstreamError(c, account, false, requestID, "http_error", payload, message)
|
||||
if status, errType, errMsg, matched := applyErrorPassthroughRule(
|
||||
c, account.Platform, 0, payload,
|
||||
http.StatusBadGateway, "api_error", message,
|
||||
); matched {
|
||||
if status == 0 {
|
||||
status = http.StatusBadGateway
|
||||
}
|
||||
if errMsg == "" {
|
||||
errMsg = message
|
||||
}
|
||||
MarkResponseCommitted(c)
|
||||
writeAnthropicError(c, status, errType, errMsg)
|
||||
return nil, fmt.Errorf("upstream response failed (passthrough): %s", errMsg)
|
||||
}
|
||||
writeAnthropicError(c, http.StatusBadGateway, "api_error", message)
|
||||
return nil, fmt.Errorf("upstream response failed: %s", message)
|
||||
}
|
||||
@@ -804,18 +818,32 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
|
||||
return true
|
||||
}
|
||||
message = s.recordOpenAIStreamUpstreamError(c, account, false, requestID, "http_error", payloadBytes, message)
|
||||
errStatus, errType, errMsg := http.StatusBadGateway, "api_error", message
|
||||
if status, et, em, matched := applyErrorPassthroughRule(
|
||||
c, account.Platform, 0, payloadBytes,
|
||||
errStatus, errType, errMsg,
|
||||
); matched {
|
||||
if status == 0 {
|
||||
status = errStatus
|
||||
}
|
||||
if em == "" {
|
||||
em = errMsg
|
||||
}
|
||||
errStatus, errType, errMsg = status, et, em
|
||||
MarkResponseCommitted(c)
|
||||
}
|
||||
if !clientDisconnected {
|
||||
if !clientOutputStarted {
|
||||
writeAnthropicError(c, http.StatusBadGateway, "api_error", message)
|
||||
writeAnthropicError(c, errStatus, errType, errMsg)
|
||||
clientOutputStarted = true
|
||||
} else {
|
||||
writeStreamHeaders()
|
||||
if _, err := fmt.Fprint(c.Writer, buildAnthropicStreamErrorSSE("api_error", message)); err == nil {
|
||||
if _, err := fmt.Fprint(c.Writer, buildAnthropicStreamErrorSSE(errType, errMsg)); err == nil {
|
||||
c.Writer.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
streamNonFailoverErr = fmt.Errorf("upstream response failed: %s", message)
|
||||
streamNonFailoverErr = fmt.Errorf("upstream response failed: %s", errMsg)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func buildContextLengthFailedSSE() string {
|
||||
failed := `{"type":"response.failed","response":{"id":"resp_err","object":"response","status":"failed","error":{"code":"context_length_exceeded","type":"invalid_request_error","message":"Your input exceeds the context window of this model. Please adjust your input and try again."},"output":[],"usage":{"input_tokens":100000,"output_tokens":0,"total_tokens":100000}}}`
|
||||
return fmt.Sprintf("data: %s\n\n", failed)
|
||||
}
|
||||
|
||||
func bindPassthroughRule(c *gin.Context, platform string, keywords []string, responseCode int) {
|
||||
svc := &ErrorPassthroughService{}
|
||||
rules := make([]*cachedPassthroughRule, 0, len(keywords))
|
||||
for i, kw := range keywords {
|
||||
code := responseCode
|
||||
rules = append(rules, &cachedPassthroughRule{
|
||||
ErrorPassthroughRule: &model.ErrorPassthroughRule{
|
||||
ID: int64(i + 1),
|
||||
Enabled: true,
|
||||
Platforms: []string{platform},
|
||||
MatchMode: model.MatchModeAny,
|
||||
Keywords: []string{kw},
|
||||
ResponseCode: &code,
|
||||
PassthroughBody: true,
|
||||
},
|
||||
lowerKeywords: []string{strings.ToLower(kw)},
|
||||
lowerPlatforms: []string{strings.ToLower(platform)},
|
||||
})
|
||||
}
|
||||
svc.localCacheMu.Lock()
|
||||
svc.localCache = rules
|
||||
svc.localCacheMu.Unlock()
|
||||
BindErrorPassthroughService(c, svc)
|
||||
}
|
||||
|
||||
func TestForwardAsChatCompletions_ResponseFailed_PassthroughRule(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
body := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"hello"}],"stream":false}`)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
bindPassthroughRule(c, "openai", []string{"context_length_exceeded"}, 400)
|
||||
|
||||
upstream := &httpUpstreamRecorder{resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
|
||||
Body: io.NopCloser(strings.NewReader(buildContextLengthFailedSSE())),
|
||||
}}
|
||||
svc := &OpenAIGatewayService{
|
||||
cfg: rawChatCompletionsTestConfig(),
|
||||
httpUpstream: upstream,
|
||||
}
|
||||
|
||||
account := rawChatCompletionsTestAccount()
|
||||
_, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "passthrough")
|
||||
require.Equal(t, 400, rec.Code, "passthrough rule should override 502 to 400")
|
||||
|
||||
respBody := rec.Body.String()
|
||||
errType := gjson.Get(respBody, "error.type").String()
|
||||
require.Equal(t, "upstream_error", errType)
|
||||
errMsg := gjson.Get(respBody, "error.message").String()
|
||||
require.NotEmpty(t, errMsg, "passthrough should preserve error message")
|
||||
require.Contains(t, errMsg, "context window")
|
||||
}
|
||||
|
||||
func TestForwardAsAnthropic_ResponseFailed_PassthroughRule(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
body := []byte(`{"model":"gpt-5.4","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
bindPassthroughRule(c, "openai", []string{"context_length_exceeded"}, 400)
|
||||
|
||||
upstream := &httpUpstreamRecorder{resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
|
||||
Body: io.NopCloser(strings.NewReader(buildContextLengthFailedSSE())),
|
||||
}}
|
||||
svc := &OpenAIGatewayService{
|
||||
cfg: rawChatCompletionsTestConfig(),
|
||||
httpUpstream: upstream,
|
||||
}
|
||||
|
||||
account := rawChatCompletionsTestAccount()
|
||||
_, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "passthrough")
|
||||
require.Equal(t, 400, rec.Code, "passthrough rule should override 502 to 400")
|
||||
respBody := rec.Body.String()
|
||||
errMsg := gjson.Get(respBody, "error.message").String()
|
||||
require.NotEmpty(t, errMsg, "passthrough should preserve error message")
|
||||
}
|
||||
|
||||
func TestForwardAsChatCompletions_ResponseFailed_NoRule_Still502(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
body := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"hello"}],"stream":false}`)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
upstream := &httpUpstreamRecorder{resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
|
||||
Body: io.NopCloser(strings.NewReader(buildContextLengthFailedSSE())),
|
||||
}}
|
||||
svc := &OpenAIGatewayService{
|
||||
cfg: rawChatCompletionsTestConfig(),
|
||||
httpUpstream: upstream,
|
||||
}
|
||||
|
||||
account := rawChatCompletionsTestAccount()
|
||||
_, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "")
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusBadGateway, rec.Code, "without passthrough rule should still be 502")
|
||||
}
|
||||
Reference in New Issue
Block a user