Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
01ef7340
Commit
01ef7340
authored
Mar 14, 2026
by
Wang Lvyuan
Browse files
Merge remote-tracking branch 'origin/main' into openai-model-mapping-fix
parents
4e8615f2
e6d59216
Changes
82
Hide whitespace changes
Inline
Side-by-side
backend/cmd/server/wire_gen.go
View file @
01ef7340
...
@@ -81,6 +81,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -81,6 +81,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
userHandler
:=
handler
.
NewUserHandler
(
userService
)
userHandler
:=
handler
.
NewUserHandler
(
userService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageBillingRepository
:=
repository
.
NewUsageBillingRepository
(
client
,
db
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
redeemHandler
:=
handler
.
NewRedeemHandler
(
redeemService
)
redeemHandler
:=
handler
.
NewRedeemHandler
(
redeemService
)
...
@@ -163,9 +164,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -163,9 +164,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
deferredService
:=
service
.
ProvideDeferredService
(
accountRepository
,
timingWheelService
)
deferredService
:=
service
.
ProvideDeferredService
(
accountRepository
,
timingWheelService
)
claudeTokenProvider
:=
service
.
NewClaudeTokenProvider
(
accountRepository
,
geminiTokenCache
,
oAuthService
)
claudeTokenProvider
:=
service
.
NewClaudeTokenProvider
(
accountRepository
,
geminiTokenCache
,
oAuthService
)
digestSessionStore
:=
service
.
NewDigestSessionStore
()
digestSessionStore
:=
service
.
NewDigestSessionStore
()
gatewayService
:=
service
.
NewGatewayService
(
accountRepository
,
groupRepository
,
usageLogRepository
,
userRepository
,
userSubscriptionRepository
,
userGroupRateRepository
,
gatewayCache
,
configConfig
,
schedulerSnapshotService
,
concurrencyService
,
billingService
,
rateLimitService
,
billingCacheService
,
identityService
,
httpUpstream
,
deferredService
,
claudeTokenProvider
,
sessionLimitCache
,
rpmCache
,
digestSessionStore
,
settingService
)
gatewayService
:=
service
.
NewGatewayService
(
accountRepository
,
groupRepository
,
usageLogRepository
,
usageBillingRepository
,
userRepository
,
userSubscriptionRepository
,
userGroupRateRepository
,
gatewayCache
,
configConfig
,
schedulerSnapshotService
,
concurrencyService
,
billingService
,
rateLimitService
,
billingCacheService
,
identityService
,
httpUpstream
,
deferredService
,
claudeTokenProvider
,
sessionLimitCache
,
rpmCache
,
digestSessionStore
,
settingService
)
openAITokenProvider
:=
service
.
NewOpenAITokenProvider
(
accountRepository
,
geminiTokenCache
,
openAIOAuthService
)
openAITokenProvider
:=
service
.
NewOpenAITokenProvider
(
accountRepository
,
geminiTokenCache
,
openAIOAuthService
)
openAIGatewayService
:=
service
.
NewOpenAIGatewayService
(
accountRepository
,
usageLogRepository
,
userRepository
,
userSubscriptionRepository
,
userGroupRateRepository
,
gatewayCache
,
configConfig
,
schedulerSnapshotService
,
concurrencyService
,
billingService
,
rateLimitService
,
billingCacheService
,
httpUpstream
,
deferredService
,
openAITokenProvider
)
openAIGatewayService
:=
service
.
NewOpenAIGatewayService
(
accountRepository
,
usageLogRepository
,
usageBillingRepository
,
userRepository
,
userSubscriptionRepository
,
userGroupRateRepository
,
gatewayCache
,
configConfig
,
schedulerSnapshotService
,
concurrencyService
,
billingService
,
rateLimitService
,
billingCacheService
,
httpUpstream
,
deferredService
,
openAITokenProvider
)
geminiMessagesCompatService
:=
service
.
NewGeminiMessagesCompatService
(
accountRepository
,
groupRepository
,
gatewayCache
,
schedulerSnapshotService
,
geminiTokenProvider
,
rateLimitService
,
httpUpstream
,
antigravityGatewayService
,
configConfig
)
geminiMessagesCompatService
:=
service
.
NewGeminiMessagesCompatService
(
accountRepository
,
groupRepository
,
gatewayCache
,
schedulerSnapshotService
,
geminiTokenProvider
,
rateLimitService
,
httpUpstream
,
antigravityGatewayService
,
configConfig
)
opsSystemLogSink
:=
service
.
ProvideOpsSystemLogSink
(
opsRepository
)
opsSystemLogSink
:=
service
.
ProvideOpsSystemLogSink
(
opsRepository
)
opsService
:=
service
.
NewOpsService
(
opsRepository
,
settingRepository
,
configConfig
,
accountRepository
,
userRepository
,
concurrencyService
,
gatewayService
,
openAIGatewayService
,
geminiMessagesCompatService
,
antigravityGatewayService
,
opsSystemLogSink
)
opsService
:=
service
.
NewOpsService
(
opsRepository
,
settingRepository
,
configConfig
,
accountRepository
,
userRepository
,
concurrencyService
,
gatewayService
,
openAIGatewayService
,
geminiMessagesCompatService
,
antigravityGatewayService
,
opsSystemLogSink
)
...
...
backend/go.mod
View file @
01ef7340
...
@@ -7,7 +7,7 @@ require (
...
@@ -7,7 +7,7 @@ require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/DouDOU-start/go-sora2api v1.1.0
github.com/DouDOU-start/go-sora2api v1.1.0
github.com/alitto/pond/v2 v2.6.2
github.com/alitto/pond/v2 v2.6.2
github.com/aws/aws-sdk-go-v2 v1.41.
2
github.com/aws/aws-sdk-go-v2 v1.41.
3
github.com/aws/aws-sdk-go-v2/config v1.32.10
github.com/aws/aws-sdk-go-v2/config v1.32.10
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
...
@@ -66,7 +66,7 @@ require (
...
@@ -66,7 +66,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/smithy-go v1.24.
1
// indirect
github.com/aws/smithy-go v1.24.
2
// indirect
github.com/bdandy/go-errors v1.2.2 // indirect
github.com/bdandy/go-errors v1.2.2 // indirect
github.com/bdandy/go-socks4 v1.2.3 // indirect
github.com/bdandy/go-socks4 v1.2.3 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
...
...
backend/go.sum
View file @
01ef7340
...
@@ -24,6 +24,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
...
@@ -24,6 +24,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
...
@@ -60,6 +62,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb8
...
@@ -60,6 +62,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb8
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
...
...
backend/internal/config/config.go
View file @
01ef7340
...
@@ -934,9 +934,10 @@ type DashboardAggregationConfig struct {
...
@@ -934,9 +934,10 @@ type DashboardAggregationConfig struct {
// DashboardAggregationRetentionConfig 预聚合保留窗口
// DashboardAggregationRetentionConfig 预聚合保留窗口
type
DashboardAggregationRetentionConfig
struct
{
type
DashboardAggregationRetentionConfig
struct
{
UsageLogsDays
int
`mapstructure:"usage_logs_days"`
UsageLogsDays
int
`mapstructure:"usage_logs_days"`
HourlyDays
int
`mapstructure:"hourly_days"`
UsageBillingDedupDays
int
`mapstructure:"usage_billing_dedup_days"`
DailyDays
int
`mapstructure:"daily_days"`
HourlyDays
int
`mapstructure:"hourly_days"`
DailyDays
int
`mapstructure:"daily_days"`
}
}
// UsageCleanupConfig 使用记录清理任务配置
// UsageCleanupConfig 使用记录清理任务配置
...
@@ -1301,6 +1302,7 @@ func setDefaults() {
...
@@ -1301,6 +1302,7 @@ func setDefaults() {
viper
.
SetDefault
(
"dashboard_aggregation.backfill_enabled"
,
false
)
viper
.
SetDefault
(
"dashboard_aggregation.backfill_enabled"
,
false
)
viper
.
SetDefault
(
"dashboard_aggregation.backfill_max_days"
,
31
)
viper
.
SetDefault
(
"dashboard_aggregation.backfill_max_days"
,
31
)
viper
.
SetDefault
(
"dashboard_aggregation.retention.usage_logs_days"
,
90
)
viper
.
SetDefault
(
"dashboard_aggregation.retention.usage_logs_days"
,
90
)
viper
.
SetDefault
(
"dashboard_aggregation.retention.usage_billing_dedup_days"
,
365
)
viper
.
SetDefault
(
"dashboard_aggregation.retention.hourly_days"
,
180
)
viper
.
SetDefault
(
"dashboard_aggregation.retention.hourly_days"
,
180
)
viper
.
SetDefault
(
"dashboard_aggregation.retention.daily_days"
,
730
)
viper
.
SetDefault
(
"dashboard_aggregation.retention.daily_days"
,
730
)
viper
.
SetDefault
(
"dashboard_aggregation.recompute_days"
,
2
)
viper
.
SetDefault
(
"dashboard_aggregation.recompute_days"
,
2
)
...
@@ -1758,6 +1760,12 @@ func (c *Config) Validate() error {
...
@@ -1758,6 +1760,12 @@ func (c *Config) Validate() error {
if
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
<=
0
{
if
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
<=
0
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_logs_days must be positive"
)
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_logs_days must be positive"
)
}
}
if
c
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
<=
0
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_billing_dedup_days must be positive"
)
}
if
c
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
<
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_billing_dedup_days must be greater than or equal to usage_logs_days"
)
}
if
c
.
DashboardAgg
.
Retention
.
HourlyDays
<=
0
{
if
c
.
DashboardAgg
.
Retention
.
HourlyDays
<=
0
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.hourly_days must be positive"
)
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.hourly_days must be positive"
)
}
}
...
@@ -1780,6 +1788,14 @@ func (c *Config) Validate() error {
...
@@ -1780,6 +1788,14 @@ func (c *Config) Validate() error {
if
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
<
0
{
if
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
<
0
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_logs_days must be non-negative"
)
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_logs_days must be non-negative"
)
}
}
if
c
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
<
0
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_billing_dedup_days must be non-negative"
)
}
if
c
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
>
0
&&
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
>
0
&&
c
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
<
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.usage_billing_dedup_days must be greater than or equal to usage_logs_days"
)
}
if
c
.
DashboardAgg
.
Retention
.
HourlyDays
<
0
{
if
c
.
DashboardAgg
.
Retention
.
HourlyDays
<
0
{
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.hourly_days must be non-negative"
)
return
fmt
.
Errorf
(
"dashboard_aggregation.retention.hourly_days must be non-negative"
)
}
}
...
...
backend/internal/config/config_test.go
View file @
01ef7340
...
@@ -441,6 +441,9 @@ func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
...
@@ -441,6 +441,9 @@ func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
if
cfg
.
DashboardAgg
.
Retention
.
UsageLogsDays
!=
90
{
if
cfg
.
DashboardAgg
.
Retention
.
UsageLogsDays
!=
90
{
t
.
Fatalf
(
"DashboardAgg.Retention.UsageLogsDays = %d, want 90"
,
cfg
.
DashboardAgg
.
Retention
.
UsageLogsDays
)
t
.
Fatalf
(
"DashboardAgg.Retention.UsageLogsDays = %d, want 90"
,
cfg
.
DashboardAgg
.
Retention
.
UsageLogsDays
)
}
}
if
cfg
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
!=
365
{
t
.
Fatalf
(
"DashboardAgg.Retention.UsageBillingDedupDays = %d, want 365"
,
cfg
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
)
}
if
cfg
.
DashboardAgg
.
Retention
.
HourlyDays
!=
180
{
if
cfg
.
DashboardAgg
.
Retention
.
HourlyDays
!=
180
{
t
.
Fatalf
(
"DashboardAgg.Retention.HourlyDays = %d, want 180"
,
cfg
.
DashboardAgg
.
Retention
.
HourlyDays
)
t
.
Fatalf
(
"DashboardAgg.Retention.HourlyDays = %d, want 180"
,
cfg
.
DashboardAgg
.
Retention
.
HourlyDays
)
}
}
...
@@ -1016,6 +1019,23 @@ func TestValidateConfigErrors(t *testing.T) {
...
@@ -1016,6 +1019,23 @@ func TestValidateConfigErrors(t *testing.T) {
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
true
;
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
=
0
},
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
true
;
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
=
0
},
wantErr
:
"dashboard_aggregation.retention.usage_logs_days"
,
wantErr
:
"dashboard_aggregation.retention.usage_logs_days"
,
},
},
{
name
:
"dashboard aggregation dedup retention"
,
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
true
c
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
=
0
},
wantErr
:
"dashboard_aggregation.retention.usage_billing_dedup_days"
,
},
{
name
:
"dashboard aggregation dedup retention smaller than usage logs"
,
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
true
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
=
30
c
.
DashboardAgg
.
Retention
.
UsageBillingDedupDays
=
29
},
wantErr
:
"dashboard_aggregation.retention.usage_billing_dedup_days"
,
},
{
{
name
:
"dashboard aggregation disabled interval"
,
name
:
"dashboard aggregation disabled interval"
,
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
false
;
c
.
DashboardAgg
.
IntervalSeconds
=
-
1
},
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
false
;
c
.
DashboardAgg
.
IntervalSeconds
=
-
1
},
...
...
backend/internal/domain/constants.go
View file @
01ef7340
...
@@ -27,10 +27,12 @@ const (
...
@@ -27,10 +27,12 @@ const (
// Account type constants
// Account type constants
const
(
const
(
AccountTypeOAuth
=
"oauth"
// OAuth类型账号(full scope: profile + inference)
AccountTypeOAuth
=
"oauth"
// OAuth类型账号(full scope: profile + inference)
AccountTypeSetupToken
=
"setup-token"
// Setup Token类型账号(inference only scope)
AccountTypeSetupToken
=
"setup-token"
// Setup Token类型账号(inference only scope)
AccountTypeAPIKey
=
"apikey"
// API Key类型账号
AccountTypeAPIKey
=
"apikey"
// API Key类型账号
AccountTypeUpstream
=
"upstream"
// 上游透传类型账号(通过 Base URL + API Key 连接上游)
AccountTypeUpstream
=
"upstream"
// 上游透传类型账号(通过 Base URL + API Key 连接上游)
AccountTypeBedrock
=
"bedrock"
// AWS Bedrock 类型账号(通过 SigV4 签名连接 Bedrock)
AccountTypeBedrockAPIKey
=
"bedrock-apikey"
// AWS Bedrock API Key 类型账号(通过 Bearer Token 连接 Bedrock)
)
)
// Redeem type constants
// Redeem type constants
...
@@ -113,3 +115,27 @@ var DefaultAntigravityModelMapping = map[string]string{
...
@@ -113,3 +115,27 @@ var DefaultAntigravityModelMapping = map[string]string{
"gpt-oss-120b-medium"
:
"gpt-oss-120b-medium"
,
"gpt-oss-120b-medium"
:
"gpt-oss-120b-medium"
,
"tab_flash_lite_preview"
:
"tab_flash_lite_preview"
,
"tab_flash_lite_preview"
:
"tab_flash_lite_preview"
,
}
}
// DefaultBedrockModelMapping 是 AWS Bedrock 平台的默认模型映射
// 将 Anthropic 标准模型名映射到 Bedrock 模型 ID
// 注意:此处的 "us." 前缀仅为默认值,ResolveBedrockModelID 会根据账号配置的
// aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等)
var
DefaultBedrockModelMapping
=
map
[
string
]
string
{
// Claude Opus
"claude-opus-4-6-thinking"
:
"us.anthropic.claude-opus-4-6-v1"
,
"claude-opus-4-6"
:
"us.anthropic.claude-opus-4-6-v1"
,
"claude-opus-4-5-thinking"
:
"us.anthropic.claude-opus-4-5-20251101-v1:0"
,
"claude-opus-4-5-20251101"
:
"us.anthropic.claude-opus-4-5-20251101-v1:0"
,
"claude-opus-4-1"
:
"us.anthropic.claude-opus-4-1-20250805-v1:0"
,
"claude-opus-4-20250514"
:
"us.anthropic.claude-opus-4-20250514-v1:0"
,
// Claude Sonnet
"claude-sonnet-4-6-thinking"
:
"us.anthropic.claude-sonnet-4-6"
,
"claude-sonnet-4-6"
:
"us.anthropic.claude-sonnet-4-6"
,
"claude-sonnet-4-5"
:
"us.anthropic.claude-sonnet-4-5-20250929-v1:0"
,
"claude-sonnet-4-5-thinking"
:
"us.anthropic.claude-sonnet-4-5-20250929-v1:0"
,
"claude-sonnet-4-5-20250929"
:
"us.anthropic.claude-sonnet-4-5-20250929-v1:0"
,
"claude-sonnet-4-20250514"
:
"us.anthropic.claude-sonnet-4-20250514-v1:0"
,
// Claude Haiku
"claude-haiku-4-5"
:
"us.anthropic.claude-haiku-4-5-20251001-v1:0"
,
"claude-haiku-4-5-20251001"
:
"us.anthropic.claude-haiku-4-5-20251001-v1:0"
,
}
backend/internal/handler/admin/account_handler.go
View file @
01ef7340
...
@@ -97,7 +97,7 @@ type CreateAccountRequest struct {
...
@@ -97,7 +97,7 @@ type CreateAccountRequest struct {
Name
string
`json:"name" binding:"required"`
Name
string
`json:"name" binding:"required"`
Notes
*
string
`json:"notes"`
Notes
*
string
`json:"notes"`
Platform
string
`json:"platform" binding:"required"`
Platform
string
`json:"platform" binding:"required"`
Type
string
`json:"type" binding:"required,oneof=oauth setup-token apikey upstream"`
Type
string
`json:"type" binding:"required,oneof=oauth setup-token apikey upstream
bedrock bedrock-apikey
"`
Credentials
map
[
string
]
any
`json:"credentials" binding:"required"`
Credentials
map
[
string
]
any
`json:"credentials" binding:"required"`
Extra
map
[
string
]
any
`json:"extra"`
Extra
map
[
string
]
any
`json:"extra"`
ProxyID
*
int64
`json:"proxy_id"`
ProxyID
*
int64
`json:"proxy_id"`
...
@@ -116,7 +116,7 @@ type CreateAccountRequest struct {
...
@@ -116,7 +116,7 @@ type CreateAccountRequest struct {
type
UpdateAccountRequest
struct
{
type
UpdateAccountRequest
struct
{
Name
string
`json:"name"`
Name
string
`json:"name"`
Notes
*
string
`json:"notes"`
Notes
*
string
`json:"notes"`
Type
string
`json:"type" binding:"omitempty,oneof=oauth setup-token apikey upstream"`
Type
string
`json:"type" binding:"omitempty,oneof=oauth setup-token apikey upstream
bedrock bedrock-apikey
"`
Credentials
map
[
string
]
any
`json:"credentials"`
Credentials
map
[
string
]
any
`json:"credentials"`
Extra
map
[
string
]
any
`json:"extra"`
Extra
map
[
string
]
any
`json:"extra"`
ProxyID
*
int64
`json:"proxy_id"`
ProxyID
*
int64
`json:"proxy_id"`
...
...
backend/internal/handler/admin/dashboard_handler.go
View file @
01ef7340
...
@@ -466,9 +466,60 @@ type BatchUsersUsageRequest struct {
...
@@ -466,9 +466,60 @@ type BatchUsersUsageRequest struct {
UserIDs
[]
int64
`json:"user_ids" binding:"required"`
UserIDs
[]
int64
`json:"user_ids" binding:"required"`
}
}
var
dashboardUsersRankingCache
=
newSnapshotCache
(
5
*
time
.
Minute
)
var
dashboardBatchUsersUsageCache
=
newSnapshotCache
(
30
*
time
.
Second
)
var
dashboardBatchUsersUsageCache
=
newSnapshotCache
(
30
*
time
.
Second
)
var
dashboardBatchAPIKeysUsageCache
=
newSnapshotCache
(
30
*
time
.
Second
)
var
dashboardBatchAPIKeysUsageCache
=
newSnapshotCache
(
30
*
time
.
Second
)
func
parseRankingLimit
(
raw
string
)
int
{
limit
,
err
:=
strconv
.
Atoi
(
strings
.
TrimSpace
(
raw
))
if
err
!=
nil
||
limit
<=
0
{
return
12
}
if
limit
>
50
{
return
50
}
return
limit
}
// GetUserSpendingRanking handles getting user spending ranking data.
// GET /api/v1/admin/dashboard/users-ranking
func
(
h
*
DashboardHandler
)
GetUserSpendingRanking
(
c
*
gin
.
Context
)
{
startTime
,
endTime
:=
parseTimeRange
(
c
)
limit
:=
parseRankingLimit
(
c
.
DefaultQuery
(
"limit"
,
"12"
))
keyRaw
,
_
:=
json
.
Marshal
(
struct
{
Start
string
`json:"start"`
End
string
`json:"end"`
Limit
int
`json:"limit"`
}{
Start
:
startTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
End
:
endTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
Limit
:
limit
,
})
cacheKey
:=
string
(
keyRaw
)
if
cached
,
ok
:=
dashboardUsersRankingCache
.
Get
(
cacheKey
);
ok
{
c
.
Header
(
"X-Snapshot-Cache"
,
"hit"
)
response
.
Success
(
c
,
cached
.
Payload
)
return
}
ranking
,
err
:=
h
.
dashboardService
.
GetUserSpendingRanking
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
limit
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get user spending ranking"
)
return
}
payload
:=
gin
.
H
{
"ranking"
:
ranking
.
Ranking
,
"total_actual_cost"
:
ranking
.
TotalActualCost
,
"start_date"
:
startTime
.
Format
(
"2006-01-02"
),
"end_date"
:
endTime
.
Add
(
-
24
*
time
.
Hour
)
.
Format
(
"2006-01-02"
),
}
dashboardUsersRankingCache
.
Set
(
cacheKey
,
payload
)
c
.
Header
(
"X-Snapshot-Cache"
,
"miss"
)
response
.
Success
(
c
,
payload
)
}
// GetBatchUsersUsage handles getting usage stats for multiple users
// GetBatchUsersUsage handles getting usage stats for multiple users
// POST /api/v1/admin/dashboard/users-usage
// POST /api/v1/admin/dashboard/users-usage
func
(
h
*
DashboardHandler
)
GetBatchUsersUsage
(
c
*
gin
.
Context
)
{
func
(
h
*
DashboardHandler
)
GetBatchUsersUsage
(
c
*
gin
.
Context
)
{
...
...
backend/internal/handler/admin/dashboard_handler_request_type_test.go
View file @
01ef7340
...
@@ -19,6 +19,9 @@ type dashboardUsageRepoCapture struct {
...
@@ -19,6 +19,9 @@ type dashboardUsageRepoCapture struct {
trendStream
*
bool
trendStream
*
bool
modelRequestType
*
int16
modelRequestType
*
int16
modelStream
*
bool
modelStream
*
bool
rankingLimit
int
ranking
[]
usagestats
.
UserSpendingRankingItem
rankingTotal
float64
}
}
func
(
s
*
dashboardUsageRepoCapture
)
GetUsageTrendWithFilters
(
func
(
s
*
dashboardUsageRepoCapture
)
GetUsageTrendWithFilters
(
...
@@ -49,6 +52,18 @@ func (s *dashboardUsageRepoCapture) GetModelStatsWithFilters(
...
@@ -49,6 +52,18 @@ func (s *dashboardUsageRepoCapture) GetModelStatsWithFilters(
return
[]
usagestats
.
ModelStat
{},
nil
return
[]
usagestats
.
ModelStat
{},
nil
}
}
func
(
s
*
dashboardUsageRepoCapture
)
GetUserSpendingRanking
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
limit
int
,
)
(
*
usagestats
.
UserSpendingRankingResponse
,
error
)
{
s
.
rankingLimit
=
limit
return
&
usagestats
.
UserSpendingRankingResponse
{
Ranking
:
s
.
ranking
,
TotalActualCost
:
s
.
rankingTotal
,
},
nil
}
func
newDashboardRequestTypeTestRouter
(
repo
*
dashboardUsageRepoCapture
)
*
gin
.
Engine
{
func
newDashboardRequestTypeTestRouter
(
repo
*
dashboardUsageRepoCapture
)
*
gin
.
Engine
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
dashboardSvc
:=
service
.
NewDashboardService
(
repo
,
nil
,
nil
,
nil
)
dashboardSvc
:=
service
.
NewDashboardService
(
repo
,
nil
,
nil
,
nil
)
...
@@ -56,6 +71,7 @@ func newDashboardRequestTypeTestRouter(repo *dashboardUsageRepoCapture) *gin.Eng
...
@@ -56,6 +71,7 @@ func newDashboardRequestTypeTestRouter(repo *dashboardUsageRepoCapture) *gin.Eng
router
:=
gin
.
New
()
router
:=
gin
.
New
()
router
.
GET
(
"/admin/dashboard/trend"
,
handler
.
GetUsageTrend
)
router
.
GET
(
"/admin/dashboard/trend"
,
handler
.
GetUsageTrend
)
router
.
GET
(
"/admin/dashboard/models"
,
handler
.
GetModelStats
)
router
.
GET
(
"/admin/dashboard/models"
,
handler
.
GetModelStats
)
router
.
GET
(
"/admin/dashboard/users-ranking"
,
handler
.
GetUserSpendingRanking
)
return
router
return
router
}
}
...
@@ -130,3 +146,30 @@ func TestDashboardModelStatsInvalidStream(t *testing.T) {
...
@@ -130,3 +146,30 @@ func TestDashboardModelStatsInvalidStream(t *testing.T) {
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
rec
.
Code
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
rec
.
Code
)
}
}
func
TestDashboardUsersRankingLimitAndCache
(
t
*
testing
.
T
)
{
dashboardUsersRankingCache
=
newSnapshotCache
(
5
*
time
.
Minute
)
repo
:=
&
dashboardUsageRepoCapture
{
ranking
:
[]
usagestats
.
UserSpendingRankingItem
{
{
UserID
:
7
,
Email
:
"rank@example.com"
,
ActualCost
:
10.5
,
Requests
:
3
,
Tokens
:
300
},
},
rankingTotal
:
88.8
,
}
router
:=
newDashboardRequestTypeTestRouter
(
repo
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/users-ranking?limit=100&start_date=2025-01-01&end_date=2025-01-02"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
50
,
repo
.
rankingLimit
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
total_actual_cost
\"
:88.8"
)
require
.
Equal
(
t
,
"miss"
,
rec
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
req2
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/users-ranking?limit=100&start_date=2025-01-01&end_date=2025-01-02"
,
nil
)
rec2
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec2
,
req2
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec2
.
Code
)
require
.
Equal
(
t
,
"hit"
,
rec2
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
}
backend/internal/handler/admin/redeem_handler.go
View file @
01ef7340
...
@@ -41,12 +41,15 @@ type GenerateRedeemCodesRequest struct {
...
@@ -41,12 +41,15 @@ type GenerateRedeemCodesRequest struct {
}
}
// CreateAndRedeemCodeRequest represents creating a fixed code and redeeming it for a target user.
// CreateAndRedeemCodeRequest represents creating a fixed code and redeeming it for a target user.
// Type 为 omitempty 而非 required 是为了向后兼容旧版调用方(不传 type 时默认 balance)。
type
CreateAndRedeemCodeRequest
struct
{
type
CreateAndRedeemCodeRequest
struct
{
Code
string
`json:"code" binding:"required,min=3,max=128"`
Code
string
`json:"code" binding:"required,min=3,max=128"`
Type
string
`json:"type" binding:"required,oneof=balance concurrency subscription invitation"`
Type
string
`json:"type" binding:"omitempty,oneof=balance concurrency subscription invitation"`
// 不传时默认 balance(向后兼容)
Value
float64
`json:"value" binding:"required,gt=0"`
Value
float64
`json:"value" binding:"required,gt=0"`
UserID
int64
`json:"user_id" binding:"required,gt=0"`
UserID
int64
`json:"user_id" binding:"required,gt=0"`
Notes
string
`json:"notes"`
GroupID
*
int64
`json:"group_id"`
// subscription 类型必填
ValidityDays
int
`json:"validity_days" binding:"omitempty,max=36500"`
// subscription 类型必填,>0
Notes
string
`json:"notes"`
}
}
// List handles listing all redeem codes with pagination
// List handles listing all redeem codes with pagination
...
@@ -136,6 +139,22 @@ func (h *RedeemHandler) CreateAndRedeem(c *gin.Context) {
...
@@ -136,6 +139,22 @@ func (h *RedeemHandler) CreateAndRedeem(c *gin.Context) {
return
return
}
}
req
.
Code
=
strings
.
TrimSpace
(
req
.
Code
)
req
.
Code
=
strings
.
TrimSpace
(
req
.
Code
)
// 向后兼容:旧版调用方(如 Sub2ApiPay)不传 type 字段,默认当作 balance 充值处理。
// 请勿删除此默认值逻辑,否则会导致旧版调用方 400 报错。
if
req
.
Type
==
""
{
req
.
Type
=
"balance"
}
if
req
.
Type
==
"subscription"
{
if
req
.
GroupID
==
nil
{
response
.
BadRequest
(
c
,
"group_id is required for subscription type"
)
return
}
if
req
.
ValidityDays
<=
0
{
response
.
BadRequest
(
c
,
"validity_days must be greater than 0 for subscription type"
)
return
}
}
executeAdminIdempotentJSON
(
c
,
"admin.redeem_codes.create_and_redeem"
,
req
,
service
.
DefaultWriteIdempotencyTTL
(),
func
(
ctx
context
.
Context
)
(
any
,
error
)
{
executeAdminIdempotentJSON
(
c
,
"admin.redeem_codes.create_and_redeem"
,
req
,
service
.
DefaultWriteIdempotencyTTL
(),
func
(
ctx
context
.
Context
)
(
any
,
error
)
{
existing
,
err
:=
h
.
redeemService
.
GetByCode
(
ctx
,
req
.
Code
)
existing
,
err
:=
h
.
redeemService
.
GetByCode
(
ctx
,
req
.
Code
)
...
@@ -147,11 +166,13 @@ func (h *RedeemHandler) CreateAndRedeem(c *gin.Context) {
...
@@ -147,11 +166,13 @@ func (h *RedeemHandler) CreateAndRedeem(c *gin.Context) {
}
}
createErr
:=
h
.
redeemService
.
CreateCode
(
ctx
,
&
service
.
RedeemCode
{
createErr
:=
h
.
redeemService
.
CreateCode
(
ctx
,
&
service
.
RedeemCode
{
Code
:
req
.
Code
,
Code
:
req
.
Code
,
Type
:
req
.
Type
,
Type
:
req
.
Type
,
Value
:
req
.
Value
,
Value
:
req
.
Value
,
Status
:
service
.
StatusUnused
,
Status
:
service
.
StatusUnused
,
Notes
:
req
.
Notes
,
Notes
:
req
.
Notes
,
GroupID
:
req
.
GroupID
,
ValidityDays
:
req
.
ValidityDays
,
})
})
if
createErr
!=
nil
{
if
createErr
!=
nil
{
// Unique code race: if code now exists, use idempotent semantics by used_by.
// Unique code race: if code now exists, use idempotent semantics by used_by.
...
...
backend/internal/handler/admin/redeem_handler_test.go
0 → 100644
View file @
01ef7340
package
admin
import
(
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newCreateAndRedeemHandler creates a RedeemHandler with a non-nil (but minimal)
// RedeemService so that CreateAndRedeem's nil guard passes and we can test the
// parameter-validation layer that runs before any service call.
func
newCreateAndRedeemHandler
()
*
RedeemHandler
{
return
&
RedeemHandler
{
adminService
:
newStubAdminService
(),
redeemService
:
&
service
.
RedeemService
{},
// non-nil to pass nil guard
}
}
// postCreateAndRedeemValidation calls CreateAndRedeem and returns the response
// status code. For cases that pass validation and proceed into the service layer,
// a panic may occur (because RedeemService internals are nil); this is expected
// and treated as "validation passed" (returns 0 to indicate panic).
func
postCreateAndRedeemValidation
(
t
*
testing
.
T
,
handler
*
RedeemHandler
,
body
any
)
(
code
int
)
{
t
.
Helper
()
gin
.
SetMode
(
gin
.
TestMode
)
w
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
w
)
jsonBytes
,
err
:=
json
.
Marshal
(
body
)
require
.
NoError
(
t
,
err
)
c
.
Request
,
_
=
http
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/redeem-codes/create-and-redeem"
,
bytes
.
NewReader
(
jsonBytes
))
c
.
Request
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
defer
func
()
{
if
r
:=
recover
();
r
!=
nil
{
// Panic means we passed validation and entered service layer (expected for minimal stub).
code
=
0
}
}()
handler
.
CreateAndRedeem
(
c
)
return
w
.
Code
}
func
TestCreateAndRedeem_TypeDefaultsToBalance
(
t
*
testing
.
T
)
{
// 不传 type 字段时应默认 balance,不触发 subscription 校验。
// 验证通过后进入 service 层会 panic(返回 0),说明默认值生效。
h
:=
newCreateAndRedeemHandler
()
code
:=
postCreateAndRedeemValidation
(
t
,
h
,
map
[
string
]
any
{
"code"
:
"test-balance-default"
,
"value"
:
10.0
,
"user_id"
:
1
,
})
assert
.
NotEqual
(
t
,
http
.
StatusBadRequest
,
code
,
"omitting type should default to balance and pass validation"
)
}
func
TestCreateAndRedeem_SubscriptionRequiresGroupID
(
t
*
testing
.
T
)
{
h
:=
newCreateAndRedeemHandler
()
code
:=
postCreateAndRedeemValidation
(
t
,
h
,
map
[
string
]
any
{
"code"
:
"test-sub-no-group"
,
"type"
:
"subscription"
,
"value"
:
29.9
,
"user_id"
:
1
,
"validity_days"
:
30
,
// group_id 缺失
})
assert
.
Equal
(
t
,
http
.
StatusBadRequest
,
code
)
}
func
TestCreateAndRedeem_SubscriptionRequiresPositiveValidityDays
(
t
*
testing
.
T
)
{
groupID
:=
int64
(
5
)
h
:=
newCreateAndRedeemHandler
()
cases
:=
[]
struct
{
name
string
validityDays
int
}{
{
"zero"
,
0
},
{
"negative"
,
-
1
},
}
for
_
,
tc
:=
range
cases
{
t
.
Run
(
tc
.
name
,
func
(
t
*
testing
.
T
)
{
code
:=
postCreateAndRedeemValidation
(
t
,
h
,
map
[
string
]
any
{
"code"
:
"test-sub-bad-days-"
+
tc
.
name
,
"type"
:
"subscription"
,
"value"
:
29.9
,
"user_id"
:
1
,
"group_id"
:
groupID
,
"validity_days"
:
tc
.
validityDays
,
})
assert
.
Equal
(
t
,
http
.
StatusBadRequest
,
code
)
})
}
}
func
TestCreateAndRedeem_SubscriptionValidParamsPassValidation
(
t
*
testing
.
T
)
{
groupID
:=
int64
(
5
)
h
:=
newCreateAndRedeemHandler
()
code
:=
postCreateAndRedeemValidation
(
t
,
h
,
map
[
string
]
any
{
"code"
:
"test-sub-valid"
,
"type"
:
"subscription"
,
"value"
:
29.9
,
"user_id"
:
1
,
"group_id"
:
groupID
,
"validity_days"
:
31
,
})
assert
.
NotEqual
(
t
,
http
.
StatusBadRequest
,
code
,
"valid subscription params should pass validation"
)
}
func
TestCreateAndRedeem_BalanceIgnoresSubscriptionFields
(
t
*
testing
.
T
)
{
h
:=
newCreateAndRedeemHandler
()
// balance 类型不传 group_id 和 validity_days,不应报 400
code
:=
postCreateAndRedeemValidation
(
t
,
h
,
map
[
string
]
any
{
"code"
:
"test-balance-no-extras"
,
"type"
:
"balance"
,
"value"
:
50.0
,
"user_id"
:
1
,
})
assert
.
NotEqual
(
t
,
http
.
StatusBadRequest
,
code
,
"balance type should not require group_id or validity_days"
)
}
backend/internal/handler/gateway_handler.go
View file @
01ef7340
...
@@ -434,19 +434,21 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
...
@@ -434,19 +434,21 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
// 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
clientIP
:=
ip
.
GetClientIP
(
c
)
clientIP
:=
ip
.
GetClientIP
(
c
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
Result
:
result
,
Result
:
result
,
APIKey
:
apiKey
,
APIKey
:
apiKey
,
User
:
apiKey
.
User
,
User
:
apiKey
.
User
,
Account
:
account
,
Account
:
account
,
Subscription
:
subscription
,
Subscription
:
subscription
,
UserAgent
:
userAgent
,
UserAgent
:
userAgent
,
IPAddress
:
clientIP
,
IPAddress
:
clientIP
,
ForceCacheBilling
:
fs
.
ForceCacheBilling
,
RequestPayloadHash
:
requestPayloadHash
,
APIKeyService
:
h
.
apiKeyService
,
ForceCacheBilling
:
fs
.
ForceCacheBilling
,
APIKeyService
:
h
.
apiKeyService
,
});
err
!=
nil
{
});
err
!=
nil
{
logger
.
L
()
.
With
(
logger
.
L
()
.
With
(
zap
.
String
(
"component"
,
"handler.gateway.messages"
),
zap
.
String
(
"component"
,
"handler.gateway.messages"
),
...
@@ -736,19 +738,21 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
...
@@ -736,19 +738,21 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
// 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
clientIP
:=
ip
.
GetClientIP
(
c
)
clientIP
:=
ip
.
GetClientIP
(
c
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
Result
:
result
,
Result
:
result
,
APIKey
:
currentAPIKey
,
APIKey
:
currentAPIKey
,
User
:
currentAPIKey
.
User
,
User
:
currentAPIKey
.
User
,
Account
:
account
,
Account
:
account
,
Subscription
:
currentSubscription
,
Subscription
:
currentSubscription
,
UserAgent
:
userAgent
,
UserAgent
:
userAgent
,
IPAddress
:
clientIP
,
IPAddress
:
clientIP
,
ForceCacheBilling
:
fs
.
ForceCacheBilling
,
RequestPayloadHash
:
requestPayloadHash
,
APIKeyService
:
h
.
apiKeyService
,
ForceCacheBilling
:
fs
.
ForceCacheBilling
,
APIKeyService
:
h
.
apiKeyService
,
});
err
!=
nil
{
});
err
!=
nil
{
logger
.
L
()
.
With
(
logger
.
L
()
.
With
(
zap
.
String
(
"component"
,
"handler.gateway.messages"
),
zap
.
String
(
"component"
,
"handler.gateway.messages"
),
...
...
backend/internal/handler/gateway_handler_warmup_intercept_unit_test.go
View file @
01ef7340
...
@@ -139,6 +139,7 @@ func newTestGatewayHandler(t *testing.T, group *service.Group, accounts []*servi
...
@@ -139,6 +139,7 @@ func newTestGatewayHandler(t *testing.T, group *service.Group, accounts []*servi
nil
,
// accountRepo (not used: scheduler snapshot hit)
nil
,
// accountRepo (not used: scheduler snapshot hit)
&
fakeGroupRepo
{
group
:
group
},
&
fakeGroupRepo
{
group
:
group
},
nil
,
// usageLogRepo
nil
,
// usageLogRepo
nil
,
// usageBillingRepo
nil
,
// userRepo
nil
,
// userRepo
nil
,
// userSubRepo
nil
,
// userSubRepo
nil
,
// userGroupRateRepo
nil
,
// userGroupRateRepo
...
...
backend/internal/handler/gemini_v1beta_handler.go
View file @
01ef7340
...
@@ -503,6 +503,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
...
@@ -503,6 +503,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
}
}
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsageWithLongContext
(
ctx
,
&
service
.
RecordUsageLongContextInput
{
if
err
:=
h
.
gatewayService
.
RecordUsageWithLongContext
(
ctx
,
&
service
.
RecordUsageLongContextInput
{
Result
:
result
,
Result
:
result
,
...
@@ -512,6 +513,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
...
@@ -512,6 +513,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
Subscription
:
subscription
,
Subscription
:
subscription
,
UserAgent
:
userAgent
,
UserAgent
:
userAgent
,
IPAddress
:
clientIP
,
IPAddress
:
clientIP
,
RequestPayloadHash
:
requestPayloadHash
,
LongContextThreshold
:
200000
,
// Gemini 200K 阈值
LongContextThreshold
:
200000
,
// Gemini 200K 阈值
LongContextMultiplier
:
2.0
,
// 超出部分双倍计费
LongContextMultiplier
:
2.0
,
// 超出部分双倍计费
ForceCacheBilling
:
fs
.
ForceCacheBilling
,
ForceCacheBilling
:
fs
.
ForceCacheBilling
,
...
...
backend/internal/handler/openai_gateway_handler.go
View file @
01ef7340
...
@@ -352,18 +352,20 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
...
@@ -352,18 +352,20 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
// 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
// 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
clientIP
:=
ip
.
GetClientIP
(
c
)
clientIP
:=
ip
.
GetClientIP
(
c
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
OpenAIRecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
OpenAIRecordUsageInput
{
Result
:
result
,
Result
:
result
,
APIKey
:
apiKey
,
APIKey
:
apiKey
,
User
:
apiKey
.
User
,
User
:
apiKey
.
User
,
Account
:
account
,
Account
:
account
,
Subscription
:
subscription
,
Subscription
:
subscription
,
UserAgent
:
userAgent
,
UserAgent
:
userAgent
,
IPAddress
:
clientIP
,
IPAddress
:
clientIP
,
APIKeyService
:
h
.
apiKeyService
,
RequestPayloadHash
:
requestPayloadHash
,
APIKeyService
:
h
.
apiKeyService
,
});
err
!=
nil
{
});
err
!=
nil
{
logger
.
L
()
.
With
(
logger
.
L
()
.
With
(
zap
.
String
(
"component"
,
"handler.openai_gateway.responses"
),
zap
.
String
(
"component"
,
"handler.openai_gateway.responses"
),
...
@@ -732,17 +734,19 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
...
@@ -732,17 +734,19 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
clientIP
:=
ip
.
GetClientIP
(
c
)
clientIP
:=
ip
.
GetClientIP
(
c
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
OpenAIRecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
OpenAIRecordUsageInput
{
Result
:
result
,
Result
:
result
,
APIKey
:
apiKey
,
APIKey
:
apiKey
,
User
:
apiKey
.
User
,
User
:
apiKey
.
User
,
Account
:
account
,
Account
:
account
,
Subscription
:
subscription
,
Subscription
:
subscription
,
UserAgent
:
userAgent
,
UserAgent
:
userAgent
,
IPAddress
:
clientIP
,
IPAddress
:
clientIP
,
APIKeyService
:
h
.
apiKeyService
,
RequestPayloadHash
:
requestPayloadHash
,
APIKeyService
:
h
.
apiKeyService
,
});
err
!=
nil
{
});
err
!=
nil
{
logger
.
L
()
.
With
(
logger
.
L
()
.
With
(
zap
.
String
(
"component"
,
"handler.openai_gateway.messages"
),
zap
.
String
(
"component"
,
"handler.openai_gateway.messages"
),
...
@@ -1231,14 +1235,15 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
...
@@ -1231,14 +1235,15 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
h
.
gatewayService
.
ReportOpenAIAccountScheduleResult
(
account
.
ID
,
true
,
result
.
FirstTokenMs
)
h
.
gatewayService
.
ReportOpenAIAccountScheduleResult
(
account
.
ID
,
true
,
result
.
FirstTokenMs
)
h
.
submitUsageRecordTask
(
func
(
taskCtx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
taskCtx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
taskCtx
,
&
service
.
OpenAIRecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
taskCtx
,
&
service
.
OpenAIRecordUsageInput
{
Result
:
result
,
Result
:
result
,
APIKey
:
apiKey
,
APIKey
:
apiKey
,
User
:
apiKey
.
User
,
User
:
apiKey
.
User
,
Account
:
account
,
Account
:
account
,
Subscription
:
subscription
,
Subscription
:
subscription
,
UserAgent
:
userAgent
,
UserAgent
:
userAgent
,
IPAddress
:
clientIP
,
IPAddress
:
clientIP
,
APIKeyService
:
h
.
apiKeyService
,
RequestPayloadHash
:
service
.
HashUsageRequestPayload
(
firstMessage
),
APIKeyService
:
h
.
apiKeyService
,
});
err
!=
nil
{
});
err
!=
nil
{
reqLog
.
Error
(
"openai.websocket_record_usage_failed"
,
reqLog
.
Error
(
"openai.websocket_record_usage_failed"
,
zap
.
Int64
(
"account_id"
,
account
.
ID
),
zap
.
Int64
(
"account_id"
,
account
.
ID
),
...
...
backend/internal/handler/sora_client_handler_test.go
View file @
01ef7340
...
@@ -2206,7 +2206,7 @@ func (s *stubSoraClientForHandler) GetVideoTask(_ context.Context, _ *service.Ac
...
@@ -2206,7 +2206,7 @@ func (s *stubSoraClientForHandler) GetVideoTask(_ context.Context, _ *service.Ac
// newMinimalGatewayService 创建仅包含 accountRepo 的最小 GatewayService(用于测试 SelectAccountForModel)。
// newMinimalGatewayService 创建仅包含 accountRepo 的最小 GatewayService(用于测试 SelectAccountForModel)。
func
newMinimalGatewayService
(
accountRepo
service
.
AccountRepository
)
*
service
.
GatewayService
{
func
newMinimalGatewayService
(
accountRepo
service
.
AccountRepository
)
*
service
.
GatewayService
{
return
service
.
NewGatewayService
(
return
service
.
NewGatewayService
(
accountRepo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
accountRepo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
)
)
}
}
...
...
backend/internal/handler/sora_gateway_handler.go
View file @
01ef7340
...
@@ -399,17 +399,19 @@ func (h *SoraGatewayHandler) ChatCompletions(c *gin.Context) {
...
@@ -399,17 +399,19 @@ func (h *SoraGatewayHandler) ChatCompletions(c *gin.Context) {
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
userAgent
:=
c
.
GetHeader
(
"User-Agent"
)
clientIP
:=
ip
.
GetClientIP
(
c
)
clientIP
:=
ip
.
GetClientIP
(
c
)
requestPayloadHash
:=
service
.
HashUsageRequestPayload
(
body
)
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
Result
:
result
,
Result
:
result
,
APIKey
:
apiKey
,
APIKey
:
apiKey
,
User
:
apiKey
.
User
,
User
:
apiKey
.
User
,
Account
:
account
,
Account
:
account
,
Subscription
:
subscription
,
Subscription
:
subscription
,
UserAgent
:
userAgent
,
UserAgent
:
userAgent
,
IPAddress
:
clientIP
,
IPAddress
:
clientIP
,
RequestPayloadHash
:
requestPayloadHash
,
});
err
!=
nil
{
});
err
!=
nil
{
logger
.
L
()
.
With
(
logger
.
L
()
.
With
(
zap
.
String
(
"component"
,
"handler.sora_gateway.chat_completions"
),
zap
.
String
(
"component"
,
"handler.sora_gateway.chat_completions"
),
...
...
backend/internal/handler/sora_gateway_handler_test.go
View file @
01ef7340
...
@@ -343,6 +343,9 @@ func (s *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, e
...
@@ -343,6 +343,9 @@ func (s *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, e
func
(
s
*
stubUsageLogRepo
)
GetUserUsageTrend
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
limit
int
)
([]
usagestats
.
UserUsageTrendPoint
,
error
)
{
func
(
s
*
stubUsageLogRepo
)
GetUserUsageTrend
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
limit
int
)
([]
usagestats
.
UserUsageTrendPoint
,
error
)
{
return
nil
,
nil
return
nil
,
nil
}
}
func
(
s
*
stubUsageLogRepo
)
GetUserSpendingRanking
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
limit
int
)
(
*
usagestats
.
UserSpendingRankingResponse
,
error
)
{
return
nil
,
nil
}
func
(
s
*
stubUsageLogRepo
)
GetBatchUserUsageStats
(
ctx
context
.
Context
,
userIDs
[]
int64
,
startTime
,
endTime
time
.
Time
)
(
map
[
int64
]
*
usagestats
.
BatchUserUsageStats
,
error
)
{
func
(
s
*
stubUsageLogRepo
)
GetBatchUserUsageStats
(
ctx
context
.
Context
,
userIDs
[]
int64
,
startTime
,
endTime
time
.
Time
)
(
map
[
int64
]
*
usagestats
.
BatchUserUsageStats
,
error
)
{
return
nil
,
nil
return
nil
,
nil
}
}
...
@@ -431,6 +434,7 @@ func TestSoraGatewayHandler_ChatCompletions(t *testing.T) {
...
@@ -431,6 +434,7 @@ func TestSoraGatewayHandler_ChatCompletions(t *testing.T) {
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
testutil
.
StubGatewayCache
{},
testutil
.
StubGatewayCache
{},
cfg
,
cfg
,
nil
,
nil
,
...
...
backend/internal/pkg/antigravity/gemini_types.go
View file @
01ef7340
...
@@ -189,6 +189,5 @@ var DefaultStopSequences = []string{
...
@@ -189,6 +189,5 @@ var DefaultStopSequences = []string{
"<|user|>"
,
"<|user|>"
,
"<|endoftext|>"
,
"<|endoftext|>"
,
"<|end_of_turn|>"
,
"<|end_of_turn|>"
,
"[DONE]"
,
"
\n\n
Human:"
,
"
\n\n
Human:"
,
}
}
backend/internal/pkg/usagestats/usage_log_types.go
View file @
01ef7340
...
@@ -96,12 +96,28 @@ type UserUsageTrendPoint struct {
...
@@ -96,12 +96,28 @@ type UserUsageTrendPoint struct {
Date
string
`json:"date"`
Date
string
`json:"date"`
UserID
int64
`json:"user_id"`
UserID
int64
`json:"user_id"`
Email
string
`json:"email"`
Email
string
`json:"email"`
Username
string
`json:"username"`
Requests
int64
`json:"requests"`
Requests
int64
`json:"requests"`
Tokens
int64
`json:"tokens"`
Tokens
int64
`json:"tokens"`
Cost
float64
`json:"cost"`
// 标准计费
Cost
float64
`json:"cost"`
// 标准计费
ActualCost
float64
`json:"actual_cost"`
// 实际扣除
ActualCost
float64
`json:"actual_cost"`
// 实际扣除
}
}
// UserSpendingRankingItem represents a user spending ranking row.
type
UserSpendingRankingItem
struct
{
UserID
int64
`json:"user_id"`
Email
string
`json:"email"`
ActualCost
float64
`json:"actual_cost"`
// 实际扣除
Requests
int64
`json:"requests"`
Tokens
int64
`json:"tokens"`
}
// UserSpendingRankingResponse represents ranking rows plus total spend for the time range.
type
UserSpendingRankingResponse
struct
{
Ranking
[]
UserSpendingRankingItem
`json:"ranking"`
TotalActualCost
float64
`json:"total_actual_cost"`
}
// APIKeyUsageTrendPoint represents API key usage trend data point
// APIKeyUsageTrendPoint represents API key usage trend data point
type
APIKeyUsageTrendPoint
struct
{
type
APIKeyUsageTrendPoint
struct
{
Date
string
`json:"date"`
Date
string
`json:"date"`
...
...
Prev
1
2
3
4
5
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment