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
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:
@@ -0,0 +1,136 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// compatCyberOAuthAccount 是 compat cyber 测试共用的 OAuth 账号。
|
||||
func compatCyberOAuthAccount() *Account {
|
||||
return &Account{
|
||||
ID: 1,
|
||||
Name: "openai-oauth",
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeOAuth,
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "oauth-token",
|
||||
"chatgpt_account_id": "chatgpt-acc",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// compatCyberUpstreamSSE 构造上游 responses SSE:response.created 后 response.failed(cyber_policy)。
|
||||
func compatCyberUpstreamSSE() string {
|
||||
return strings.Join([]string{
|
||||
`data: {"type":"response.created","response":{"id":"resp_cyber","model":"gpt-5.5","status":"in_progress","output":[]}}`,
|
||||
"",
|
||||
`event: response.failed`,
|
||||
`data: {"type":"response.failed","response":{"id":"resp_cyber","object":"response","model":"gpt-5.5","status":"failed","output":[],"error":{"code":"cyber_policy","message":"flagged for cyber policy"}}}`,
|
||||
"",
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func compatCyberUpstreamRecorder() *httpUpstreamRecorder {
|
||||
return &httpUpstreamRecorder{resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_cyber"}},
|
||||
Body: io.NopCloser(strings.NewReader(compatCyberUpstreamSSE())),
|
||||
}}
|
||||
}
|
||||
|
||||
// C-1: chat completions 非流式客户端(buffered 路径)cyber 命中——不 failover、标记已设、
|
||||
// 以 chat 错误格式回写、丢弃 result(使 handler 落入 tokens=0 免费用量行而非 RecordUsage 扣费)。
|
||||
func TestForwardAsChatCompletions_BufferedCyberPolicyNoFailover(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
body := []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hi"}],"stream":false}`)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
svc := &OpenAIGatewayService{httpUpstream: compatCyberUpstreamRecorder()}
|
||||
|
||||
result, err := svc.ForwardAsChatCompletions(context.Background(), c, compatCyberOAuthAccount(), body, "", "gpt-5.5")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result, "cyber must drop result so handler writes tokens=0 free row, not RecordUsage")
|
||||
var failoverErr *UpstreamFailoverError
|
||||
require.False(t, errors.As(err, &failoverErr), "cyber must NOT trigger failover")
|
||||
mark := GetOpsCyberPolicy(c)
|
||||
require.NotNil(t, mark, "cyber mark must be set for handler-side recording")
|
||||
require.Equal(t, "cyber_policy", mark.Code)
|
||||
require.True(t, c.Writer.Written(), "cyber error must be written to client (passthrough)")
|
||||
}
|
||||
|
||||
// I-1: chat completions 流式客户端 cyber 命中——result 必须被丢弃(返回 nil),
|
||||
// 使 handler forwardErrored 分支走 tokens=0 免费行,而非 RecordUsage(CyberBlocked) 扣费。
|
||||
func TestForwardAsChatCompletions_StreamCyberPolicyDropsResult(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
body := []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hi"}],"stream":true}`)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
svc := &OpenAIGatewayService{httpUpstream: compatCyberUpstreamRecorder()}
|
||||
|
||||
result, err := svc.ForwardAsChatCompletions(context.Background(), c, compatCyberOAuthAccount(), body, "", "gpt-5.5")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result, "cyber must drop result so handler does not bill via RecordUsage")
|
||||
var failoverErr *UpstreamFailoverError
|
||||
require.False(t, errors.As(err, &failoverErr), "cyber must NOT trigger failover")
|
||||
require.NotNil(t, GetOpsCyberPolicy(c), "cyber mark must be set")
|
||||
require.Contains(t, rec.Body.String(), "data: [DONE]", "stream must terminate with [DONE]")
|
||||
}
|
||||
|
||||
// anthropic 非流式客户端(buffered 路径)cyber 命中——不 failover、标记已设、以 anthropic 错误格式回写、丢弃 result。
|
||||
func TestForwardAsAnthropic_BufferedCyberPolicyNoFailover(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
body := []byte(`{"model":"gpt-5.5","max_tokens":1024,"messages":[{"role":"user","content":"hi"}],"stream":false}`)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
svc := &OpenAIGatewayService{httpUpstream: compatCyberUpstreamRecorder()}
|
||||
|
||||
result, err := svc.ForwardAsAnthropic(context.Background(), c, compatCyberOAuthAccount(), body, "", "gpt-5.5")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result, "cyber must drop result so handler writes tokens=0 free row")
|
||||
var failoverErr *UpstreamFailoverError
|
||||
require.False(t, errors.As(err, &failoverErr), "cyber must NOT trigger failover")
|
||||
mark := GetOpsCyberPolicy(c)
|
||||
require.NotNil(t, mark, "cyber mark must be set")
|
||||
require.Equal(t, "cyber_policy", mark.Code)
|
||||
require.True(t, c.Writer.Written(), "anthropic cyber error must be written to client")
|
||||
require.Contains(t, rec.Body.String(), `"type":"error"`, "must use anthropic error envelope")
|
||||
}
|
||||
|
||||
// anthropic 流式客户端 cyber 命中——不 failover、标记已设、下发 anthropic SSE error 事件、丢弃 result。
|
||||
func TestForwardAsAnthropic_StreamCyberPolicyNoFailover(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
body := []byte(`{"model":"gpt-5.5","max_tokens":1024,"messages":[{"role":"user","content":"hi"}],"stream":true}`)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
svc := &OpenAIGatewayService{httpUpstream: compatCyberUpstreamRecorder()}
|
||||
|
||||
result, err := svc.ForwardAsAnthropic(context.Background(), c, compatCyberOAuthAccount(), body, "", "gpt-5.5")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result, "cyber must drop result so handler does not bill via RecordUsage")
|
||||
var failoverErr *UpstreamFailoverError
|
||||
require.False(t, errors.As(err, &failoverErr), "cyber must NOT trigger failover")
|
||||
require.NotNil(t, GetOpsCyberPolicy(c), "cyber mark must be set")
|
||||
require.Contains(t, rec.Body.String(), "event: error", "must emit anthropic SSE error event")
|
||||
}
|
||||
Reference in New Issue
Block a user