Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
@@ -0,0 +1,135 @@
package service
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestOpenAIRequestBodyLimitFailover_HTTP413SwitchesAccountsBeforeWrite(t *testing.T) {
gin.SetMode(gin.TestMode)
requestBody := []byte(`{"model":"gpt-5.2","stream":false,"input":"hello"}`)
for _, passthrough := range []bool{false, true} {
name := "native_responses"
if passthrough {
name = "api_key_passthrough"
}
t.Run(name, func(t *testing.T) {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
const upstreamBody = `{"error":{"message":"request body exceeds this account's 16MB proxy limit; secret=must-not-leak","type":"invalid_request_error"}}`
body := &passthroughCloseTrackingReadCloser{Reader: strings.NewReader(upstreamBody)}
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusRequestEntityTooLarge,
Header: http.Header{
"Content-Type": []string{"application/json"},
"X-Request-Id": []string{"rid-body-limit"},
},
Body: body,
}}
svc := &OpenAIGatewayService{
cfg: &config.Config{Gateway: config.GatewayConfig{ForceCodexCLI: false}},
httpUpstream: upstream,
}
account := &Account{
ID: 161,
Name: name,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Concurrency: 1,
Credentials: map[string]any{
"api_key": "sk-test",
"base_url": "https://api.example.test",
"pool_mode": true,
"pool_mode_retry_status_codes": []any{
float64(http.StatusRequestEntityTooLarge),
},
},
Extra: map[string]any{
"openai_passthrough": passthrough,
"openai_responses_supported": true,
},
Status: StatusActive,
Schedulable: true,
}
result, err := svc.Forward(context.Background(), c, account, requestBody)
require.Nil(t, result)
var failoverErr *UpstreamFailoverError
require.ErrorAs(t, err, &failoverErr)
require.Equal(t, http.StatusRequestEntityTooLarge, failoverErr.StatusCode)
require.Equal(t, GatewayFailureScopeAccount, failoverErr.Scope)
require.Equal(t, GatewayFailureReason("openai_request_body_too_large"), failoverErr.Reason)
require.Equal(t, NextAccountRetry, failoverErr.NextAccountAction)
require.Equal(t, http.StatusRequestEntityTooLarge, failoverErr.ClientStatusCode)
require.Equal(t, "Request payload is too large", failoverErr.ClientMessage)
require.False(t, failoverErr.RetryableOnSameAccount, "a body limit requires another account, not another attempt on the same account")
require.False(t, c.Writer.Written(), "account failover must happen before downstream output is committed")
require.Empty(t, rec.Body.String())
require.True(t, body.closed)
if passthrough {
require.Equal(t, requestBody, upstream.lastBody)
} else {
require.Equal(t, "gpt-5.2", gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, "hello", gjson.GetBytes(upstream.lastBody, "input").String())
}
})
}
}
func TestOpenAIRequestBodyLimitFailover_ContextWindow413DoesNotSwitchAccounts(t *testing.T) {
gin.SetMode(gin.TestMode)
requestBody := []byte(`{"model":"gpt-5.2","stream":false,"input":"hello"}`)
for _, passthrough := range []bool{false, true} {
t.Run(fmt.Sprintf("passthrough_%t", passthrough), func(t *testing.T) {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
const upstreamBody = `{"error":{"message":"Your input exceeds the context window of this model. Please adjust your input and try again.","type":"invalid_request_error"}}`
body := &passthroughCloseTrackingReadCloser{Reader: strings.NewReader(upstreamBody)}
svc := &OpenAIGatewayService{
cfg: &config.Config{Gateway: config.GatewayConfig{ForceCodexCLI: false}},
httpUpstream: &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusRequestEntityTooLarge,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: body,
}},
}
account := &Account{
ID: 162, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Concurrency: 1,
Credentials: map[string]any{"api_key": "sk-test", "base_url": "https://api.example.test"},
Extra: map[string]any{
"openai_passthrough": passthrough,
"openai_responses_supported": true,
},
Status: StatusActive, Schedulable: true,
}
result, err := svc.Forward(context.Background(), c, account, requestBody)
require.Nil(t, result)
require.Error(t, err)
var failoverErr *UpstreamFailoverError
require.False(t, errors.As(err, &failoverErr), "context-window failures are deterministic request errors")
require.True(t, c.Writer.Written())
require.Contains(t, rec.Body.String(), "exceeds the context window")
require.True(t, body.closed)
})
}
}