mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-25 09:04:45 +08:00
fix(antigravity): 固定按映射模型计费并补充回归测试
当账号配置了 model_mapping 时,确保计费使用映射后的实际模型, 而非用户请求的原始模型名,避免计费不准确。
This commit is contained in:
2
.github/workflows/security-scan.yml
vendored
2
.github/workflows/security-scan.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
working-directory: backend
|
working-directory: backend
|
||||||
run: |
|
run: |
|
||||||
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||||
gosec -severity high -confidence high ./...
|
gosec -severity high -confidence high -exclude=G704 ./...
|
||||||
|
|
||||||
frontend-security:
|
frontend-security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
antigravityBillingModelEnv = "GATEWAY_ANTIGRAVITY_BILL_WITH_MAPPED_MODEL"
|
|
||||||
antigravityForwardBaseURLEnv = "GATEWAY_ANTIGRAVITY_FORWARD_BASE_URL"
|
antigravityForwardBaseURLEnv = "GATEWAY_ANTIGRAVITY_FORWARD_BASE_URL"
|
||||||
antigravityFallbackSecondsEnv = "GATEWAY_ANTIGRAVITY_FALLBACK_COOLDOWN_SECONDS"
|
antigravityFallbackSecondsEnv = "GATEWAY_ANTIGRAVITY_FALLBACK_COOLDOWN_SECONDS"
|
||||||
)
|
)
|
||||||
@@ -1311,6 +1310,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
|||||||
// 应用 thinking 模式自动后缀:如果 thinking 开启且目标是 claude-sonnet-4-5,自动改为 thinking 版本
|
// 应用 thinking 模式自动后缀:如果 thinking 开启且目标是 claude-sonnet-4-5,自动改为 thinking 版本
|
||||||
thinkingEnabled := claudeReq.Thinking != nil && (claudeReq.Thinking.Type == "enabled" || claudeReq.Thinking.Type == "adaptive")
|
thinkingEnabled := claudeReq.Thinking != nil && (claudeReq.Thinking.Type == "enabled" || claudeReq.Thinking.Type == "adaptive")
|
||||||
mappedModel = applyThinkingModelSuffix(mappedModel, thinkingEnabled)
|
mappedModel = applyThinkingModelSuffix(mappedModel, thinkingEnabled)
|
||||||
|
billingModel := mappedModel
|
||||||
|
|
||||||
// 获取 access_token
|
// 获取 access_token
|
||||||
if s.tokenProvider == nil {
|
if s.tokenProvider == nil {
|
||||||
@@ -1624,7 +1624,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
|||||||
return &ForwardResult{
|
return &ForwardResult{
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
Usage: *usage,
|
Usage: *usage,
|
||||||
Model: originalModel, // 使用原始模型用于计费和日志
|
Model: billingModel, // 使用映射模型用于计费和日志
|
||||||
Stream: claudeReq.Stream,
|
Stream: claudeReq.Stream,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
@@ -1978,6 +1978,7 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
|||||||
if mappedModel == "" {
|
if mappedModel == "" {
|
||||||
return nil, s.writeGoogleError(c, http.StatusForbidden, fmt.Sprintf("model %s not in whitelist", originalModel))
|
return nil, s.writeGoogleError(c, http.StatusForbidden, fmt.Sprintf("model %s not in whitelist", originalModel))
|
||||||
}
|
}
|
||||||
|
billingModel := mappedModel
|
||||||
|
|
||||||
// 获取 access_token
|
// 获取 access_token
|
||||||
if s.tokenProvider == nil {
|
if s.tokenProvider == nil {
|
||||||
@@ -2207,7 +2208,7 @@ handleSuccess:
|
|||||||
return &ForwardResult{
|
return &ForwardResult{
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
Usage: *usage,
|
Usage: *usage,
|
||||||
Model: originalModel,
|
Model: billingModel,
|
||||||
Stream: stream,
|
Stream: stream,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
@@ -3883,7 +3884,6 @@ func (s *AntigravityGatewayService) ForwardUpstream(ctx context.Context, c *gin.
|
|||||||
return nil, fmt.Errorf("missing model")
|
return nil, fmt.Errorf("missing model")
|
||||||
}
|
}
|
||||||
originalModel := claudeReq.Model
|
originalModel := claudeReq.Model
|
||||||
billingModel := originalModel
|
|
||||||
|
|
||||||
// 构建上游请求 URL
|
// 构建上游请求 URL
|
||||||
upstreamURL := baseURL + "/v1/messages"
|
upstreamURL := baseURL + "/v1/messages"
|
||||||
@@ -3936,7 +3936,7 @@ func (s *AntigravityGatewayService) ForwardUpstream(ctx context.Context, c *gin.
|
|||||||
_, _ = c.Writer.Write(respBody)
|
_, _ = c.Writer.Write(respBody)
|
||||||
|
|
||||||
return &ForwardResult{
|
return &ForwardResult{
|
||||||
Model: billingModel,
|
Model: originalModel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3977,7 +3977,7 @@ func (s *AntigravityGatewayService) ForwardUpstream(ctx context.Context, c *gin.
|
|||||||
log.Printf("%s status=success duration_ms=%d", prefix, duration.Milliseconds())
|
log.Printf("%s status=success duration_ms=%d", prefix, duration.Milliseconds())
|
||||||
|
|
||||||
return &ForwardResult{
|
return &ForwardResult{
|
||||||
Model: billingModel,
|
Model: originalModel,
|
||||||
Stream: claudeReq.Stream,
|
Stream: claudeReq.Stream,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
FirstTokenMs: firstTokenMs,
|
FirstTokenMs: firstTokenMs,
|
||||||
|
|||||||
@@ -133,6 +133,36 @@ func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int,
|
|||||||
return s.resp, s.err
|
return s.resp, s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type antigravitySettingRepoStub struct{}
|
||||||
|
|
||||||
|
func (s *antigravitySettingRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
|
||||||
|
panic("unexpected Get call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *antigravitySettingRepoStub) GetValue(ctx context.Context, key string) (string, error) {
|
||||||
|
return "", ErrSettingNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *antigravitySettingRepoStub) Set(ctx context.Context, key, value string) error {
|
||||||
|
panic("unexpected Set call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *antigravitySettingRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
||||||
|
panic("unexpected GetMultiple call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *antigravitySettingRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
|
||||||
|
panic("unexpected SetMultiple call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *antigravitySettingRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
|
||||||
|
panic("unexpected GetAll call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *antigravitySettingRepoStub) Delete(ctx context.Context, key string) error {
|
||||||
|
panic("unexpected Delete call")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAntigravityGatewayService_Forward_PromptTooLong(t *testing.T) {
|
func TestAntigravityGatewayService_Forward_PromptTooLong(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
writer := httptest.NewRecorder()
|
writer := httptest.NewRecorder()
|
||||||
@@ -159,6 +189,7 @@ func TestAntigravityGatewayService_Forward_PromptTooLong(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
svc := &AntigravityGatewayService{
|
svc := &AntigravityGatewayService{
|
||||||
|
settingService: NewSettingService(&antigravitySettingRepoStub{}, &config.Config{Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize}}),
|
||||||
tokenProvider: &AntigravityTokenProvider{},
|
tokenProvider: &AntigravityTokenProvider{},
|
||||||
httpUpstream: &httpUpstreamStub{resp: resp},
|
httpUpstream: &httpUpstreamStub{resp: resp},
|
||||||
}
|
}
|
||||||
@@ -417,6 +448,113 @@ func TestAntigravityGatewayService_ForwardGemini_StickySessionForceCacheBilling(
|
|||||||
require.True(t, failoverErr.ForceCacheBilling, "ForceCacheBilling should be true for sticky session switch")
|
require.True(t, failoverErr.ForceCacheBilling, "ForceCacheBilling should be true for sticky session switch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAntigravityGatewayService_Forward_BillsWithMappedModel
|
||||||
|
// 验证:Antigravity Claude 转发返回的计费模型使用映射后的模型
|
||||||
|
func TestAntigravityGatewayService_Forward_BillsWithMappedModel(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(writer)
|
||||||
|
|
||||||
|
body, err := json.Marshal(map[string]any{
|
||||||
|
"model": "claude-sonnet-4-5",
|
||||||
|
"messages": []map[string]any{
|
||||||
|
{"role": "user", "content": "hello"},
|
||||||
|
},
|
||||||
|
"max_tokens": 16,
|
||||||
|
"stream": true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
|
||||||
|
c.Request = req
|
||||||
|
|
||||||
|
upstreamBody := []byte("data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"ok\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":8,\"candidatesTokenCount\":3}}}\n\n")
|
||||||
|
resp := &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Header: http.Header{"X-Request-Id": []string{"req-bill-1"}},
|
||||||
|
Body: io.NopCloser(bytes.NewReader(upstreamBody)),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := &AntigravityGatewayService{
|
||||||
|
settingService: NewSettingService(&antigravitySettingRepoStub{}, &config.Config{Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize}}),
|
||||||
|
tokenProvider: &AntigravityTokenProvider{},
|
||||||
|
httpUpstream: &httpUpstreamStub{resp: resp},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedModel = "gemini-3-pro-high"
|
||||||
|
account := &Account{
|
||||||
|
ID: 5,
|
||||||
|
Name: "acc-forward-billing",
|
||||||
|
Platform: PlatformAntigravity,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Status: StatusActive,
|
||||||
|
Concurrency: 1,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"access_token": "token",
|
||||||
|
"model_mapping": map[string]any{
|
||||||
|
"claude-sonnet-4-5": mappedModel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := svc.Forward(context.Background(), c, account, body, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, result)
|
||||||
|
require.Equal(t, mappedModel, result.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAntigravityGatewayService_ForwardGemini_BillsWithMappedModel
|
||||||
|
// 验证:Antigravity Gemini 转发返回的计费模型使用映射后的模型
|
||||||
|
func TestAntigravityGatewayService_ForwardGemini_BillsWithMappedModel(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(writer)
|
||||||
|
|
||||||
|
body, err := json.Marshal(map[string]any{
|
||||||
|
"contents": []map[string]any{
|
||||||
|
{"role": "user", "parts": []map[string]any{{"text": "hello"}}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1beta/models/gemini-2.5-flash:generateContent", bytes.NewReader(body))
|
||||||
|
c.Request = req
|
||||||
|
|
||||||
|
upstreamBody := []byte("data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"ok\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":8,\"candidatesTokenCount\":3}}}\n\n")
|
||||||
|
resp := &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Header: http.Header{"X-Request-Id": []string{"req-bill-2"}},
|
||||||
|
Body: io.NopCloser(bytes.NewReader(upstreamBody)),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := &AntigravityGatewayService{
|
||||||
|
settingService: NewSettingService(&antigravitySettingRepoStub{}, &config.Config{Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize}}),
|
||||||
|
tokenProvider: &AntigravityTokenProvider{},
|
||||||
|
httpUpstream: &httpUpstreamStub{resp: resp},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedModel = "gemini-3-pro-high"
|
||||||
|
account := &Account{
|
||||||
|
ID: 6,
|
||||||
|
Name: "acc-gemini-billing",
|
||||||
|
Platform: PlatformAntigravity,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Status: StatusActive,
|
||||||
|
Concurrency: 1,
|
||||||
|
Credentials: map[string]any{
|
||||||
|
"access_token": "token",
|
||||||
|
"model_mapping": map[string]any{
|
||||||
|
"gemini-2.5-flash": mappedModel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := svc.ForwardGemini(context.Background(), c, account, "gemini-2.5-flash", "generateContent", true, body, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, result)
|
||||||
|
require.Equal(t, mappedModel, result.Model)
|
||||||
|
}
|
||||||
|
|
||||||
// --- 流式 happy path 测试 ---
|
// --- 流式 happy path 测试 ---
|
||||||
|
|
||||||
// TestStreamUpstreamResponse_NormalComplete
|
// TestStreamUpstreamResponse_NormalComplete
|
||||||
|
|||||||
Reference in New Issue
Block a user