mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
- 新增 241 迁移:user_platform_quotas / composite_model_routes 的 CHECK 约束 加入 typesafe。此前设置 typesafe 默认配额会让注册时的多行配额快照整体 违约(fail-open 后新用户所有平台配额丢失),单用户配额与 Composite 路由保存 500。 - Prompt 审计新增 typesafe_systemone 协议提取(state + 各问题 instructions/ criteria/选项标签,键排序保证哈希稳定);此前提取为空,阻断与异步审计均被放行。 旧内容审核同样覆盖问题文本,且不再丢弃含 <system-reminder> 的 System One 文本。 - TypeSafe 分组及路由到 TypeSafe 的 Composite 请求访问 Messages / count_tokens / Chat Completions / Responses 时返回 404,避免以 x-api-key 打到错误上游路径并 污染账号状态;TypeSafe 账号 base_url 为空时不再回退 api.anthropic.com。 - SystemOne 补在途余额预留、利润控制准入终检、wrapReleaseOnDone,失败切换改用 共享 FailoverState(同账号重试 / 池模式 / 临时封禁)。 - 上游错误沿用共享账号错误策略:402/403、自定义错误码、临时不可调度规则生效, 记录 ops 上游错误;400/422 仍不切换且永不改变账号状态。 - 账号测试改走原生 System One 探测,可用模型只返回 jev-latest; 响应超限显式报错,响应 Content-Type 仅透传 JSON 类型。
110 lines
4.9 KiB
Go
110 lines
4.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const validSystemOneHandlerBody = `{"model":"jev-latest","state":"sample","questions":{"q":{"type":"noul","instructions":"Evaluate"}}}`
|
|
|
|
func newSystemOneHandlerContext(body string) (*gin.Context, *httptest.ResponseRecorder) {
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/v1/systemone", strings.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
return c, recorder
|
|
}
|
|
|
|
func TestSystemOneRequiresAuthentication(t *testing.T) {
|
|
c, recorder := newSystemOneHandlerContext(validSystemOneHandlerBody)
|
|
(&GatewayHandler{}).SystemOne(c)
|
|
require.Equal(t, http.StatusUnauthorized, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), "authentication_error")
|
|
}
|
|
|
|
func TestSystemOneRejectsNonTypeSafeGroupBeforeScheduling(t *testing.T) {
|
|
c, recorder := newSystemOneHandlerContext(validSystemOneHandlerBody)
|
|
groupID := int64(3)
|
|
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{ID: 4, UserID: 5, GroupID: &groupID, Group: &service.Group{ID: groupID, Platform: service.PlatformOpenAI}})
|
|
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: 5, Concurrency: 1})
|
|
|
|
(&GatewayHandler{cfg: &config.Config{Gateway: config.GatewayConfig{MaxBodySize: 1 << 20}}}).SystemOne(c)
|
|
require.Equal(t, http.StatusNotFound, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), "only available for TypeSafe")
|
|
}
|
|
|
|
func newTypeSafeGroupContext(t *testing.T, path, body, groupPlatform string) (*gin.Context, *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
c, recorder := newSystemOneHandlerContext(body)
|
|
c.Request = httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
groupID := int64(9)
|
|
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{ID: 4, UserID: 5, GroupID: &groupID, Group: &service.Group{ID: groupID, Platform: groupPlatform}})
|
|
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: 5, Concurrency: 1})
|
|
return c, recorder
|
|
}
|
|
|
|
func TestRejectSystemOneOnlyPlatform(t *testing.T) {
|
|
write := func(c *gin.Context, status int, errType, message string) {
|
|
c.JSON(status, gin.H{"type": errType, "message": message})
|
|
}
|
|
|
|
c, recorder := newTypeSafeGroupContext(t, "/v1/messages", `{}`, service.PlatformTypeSafe)
|
|
require.True(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
|
|
require.Equal(t, http.StatusNotFound, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), systemOneOnlyPlatformMessage)
|
|
|
|
c, _ = newTypeSafeGroupContext(t, "/v1/messages", `{}`, service.PlatformComposite)
|
|
require.False(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
|
|
c.Request = c.Request.WithContext(service.WithResolvedTargetPlatform(c.Request.Context(), service.PlatformTypeSafe))
|
|
require.True(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
|
|
|
|
c, _ = newTypeSafeGroupContext(t, "/v1/messages", `{}`, service.PlatformAnthropic)
|
|
require.False(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
|
|
|
|
c, _ = newTypeSafeGroupContext(t, "/antigravity/v1/messages", `{}`, service.PlatformTypeSafe)
|
|
c.Set(string(middleware2.ContextKeyForcePlatform), service.PlatformAntigravity)
|
|
require.False(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
|
|
}
|
|
|
|
func mustAPIKey(t *testing.T, c *gin.Context) *service.APIKey {
|
|
t.Helper()
|
|
apiKey, ok := middleware2.GetAPIKeyFromContext(c)
|
|
require.True(t, ok)
|
|
return apiKey
|
|
}
|
|
|
|
func TestTypeSafeGroupsRejectNonSystemOneProtocolsBeforeScheduling(t *testing.T) {
|
|
h := &GatewayHandler{
|
|
cfg: &config.Config{Gateway: config.GatewayConfig{MaxBodySize: 1 << 20}},
|
|
gatewayService: &service.GatewayService{},
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
path string
|
|
body string
|
|
handler func(*gin.Context)
|
|
}{
|
|
{"messages", "/v1/messages", `{"model":"jev-latest","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}`, h.Messages},
|
|
{"count_tokens", "/v1/messages/count_tokens", `{"model":"jev-latest","messages":[{"role":"user","content":"hi"}]}`, h.CountTokens},
|
|
{"chat_completions", "/v1/chat/completions", `{"model":"jev-latest","messages":[{"role":"user","content":"hi"}]}`, h.ChatCompletions},
|
|
{"responses", "/v1/responses", `{"model":"jev-latest","input":"hi"}`, h.Responses},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c, recorder := newTypeSafeGroupContext(t, tc.path, tc.body, service.PlatformTypeSafe)
|
|
tc.handler(c)
|
|
require.Equal(t, http.StatusNotFound, recorder.Code, recorder.Body.String())
|
|
require.Contains(t, recorder.Body.String(), systemOneOnlyPlatformMessage)
|
|
})
|
|
}
|
|
}
|