上游对单次请求下发 error.code=cyber_policy 硬阻断时,网关在所有端点 (/v1/responses、/v1/chat/completions、/v1/messages、WebSocket)及流式/ 非流式路径下,将该结果原样透传给客户端,绝不 failover、换号或同步拦截; 命中后异步完成审计与计费: - 风控中心记录 cyber_policy 留痕并发送通知邮件,落库先于发信,SMTP 阻塞 不影响留痕 - ops 错误请求记录,状态码对齐客户端实际接收(流式 200 / 非流式 400) - 用量明细标记 request_type=cyber,按上游真实 token 计费,HTTP 与 WebSocket 计费口径统一,零 token 命中不误扣 - 会话级自动屏蔽(管理员开关,默认关):命中的会话在可配 TTL 内本地拦截 不再发往上游,仅屏蔽该会话不影响同 Key 其他会话 - 封号计数排除开关:可选让 cyber 命中不计入自动封号,命中当次不判定且 历史行在违规计数中一并排除 WebSocket 多轮连接下 cyber 标记按 turn 生命周期管理,逐轮独立检测与记录; 透传的错误响应不被兜底逻辑追加内容污染。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
209 lines
5.5 KiB
Go
209 lines
5.5 KiB
Go
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
BillingTypeBalance int8 = 0 // 钱包余额
|
||
BillingTypeSubscription int8 = 1 // 订阅套餐
|
||
)
|
||
|
||
type RequestType int16
|
||
|
||
const (
|
||
RequestTypeUnknown RequestType = 0
|
||
RequestTypeSync RequestType = 1
|
||
RequestTypeStream RequestType = 2
|
||
RequestTypeWSV2 RequestType = 3
|
||
RequestTypeCyberBlocked RequestType = 4 // cyber_policy 命中(透传但被上游安全策略拒绝)
|
||
)
|
||
|
||
func (t RequestType) IsValid() bool {
|
||
switch t {
|
||
case RequestTypeUnknown, RequestTypeSync, RequestTypeStream, RequestTypeWSV2, RequestTypeCyberBlocked:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func (t RequestType) Normalize() RequestType {
|
||
if t.IsValid() {
|
||
return t
|
||
}
|
||
return RequestTypeUnknown
|
||
}
|
||
|
||
func (t RequestType) String() string {
|
||
switch t.Normalize() {
|
||
case RequestTypeSync:
|
||
return "sync"
|
||
case RequestTypeStream:
|
||
return "stream"
|
||
case RequestTypeWSV2:
|
||
return "ws_v2"
|
||
case RequestTypeCyberBlocked:
|
||
return "cyber"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
func RequestTypeFromInt16(v int16) RequestType {
|
||
return RequestType(v).Normalize()
|
||
}
|
||
|
||
func ParseUsageRequestType(value string) (RequestType, error) {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "unknown":
|
||
return RequestTypeUnknown, nil
|
||
case "sync":
|
||
return RequestTypeSync, nil
|
||
case "stream":
|
||
return RequestTypeStream, nil
|
||
case "ws_v2":
|
||
return RequestTypeWSV2, nil
|
||
case "cyber":
|
||
return RequestTypeCyberBlocked, nil
|
||
default:
|
||
return RequestTypeUnknown, fmt.Errorf("invalid request_type, allowed values: unknown, sync, stream, ws_v2, cyber")
|
||
}
|
||
}
|
||
|
||
func RequestTypeFromLegacy(stream bool, openAIWSMode bool) RequestType {
|
||
if openAIWSMode {
|
||
return RequestTypeWSV2
|
||
}
|
||
if stream {
|
||
return RequestTypeStream
|
||
}
|
||
return RequestTypeSync
|
||
}
|
||
|
||
func ApplyLegacyRequestFields(requestType RequestType, fallbackStream bool, fallbackOpenAIWSMode bool) (stream bool, openAIWSMode bool) {
|
||
switch requestType.Normalize() {
|
||
case RequestTypeSync:
|
||
return false, false
|
||
case RequestTypeStream:
|
||
return true, false
|
||
case RequestTypeWSV2:
|
||
return true, true
|
||
default:
|
||
return fallbackStream, fallbackOpenAIWSMode
|
||
}
|
||
}
|
||
|
||
type UsageLog struct {
|
||
ID int64
|
||
UserID int64
|
||
APIKeyID int64
|
||
AccountID int64
|
||
RequestID string
|
||
Model string
|
||
// RequestedModel is the client-requested model name recorded for stable user/admin display.
|
||
// Empty should be treated as Model for backward compatibility with historical rows.
|
||
RequestedModel string
|
||
// UpstreamModel is the actual model sent to the upstream provider after mapping.
|
||
// Nil means no mapping was applied (requested model was used as-is).
|
||
UpstreamModel *string
|
||
// ChannelID 渠道 ID
|
||
ChannelID *int64
|
||
// ModelMappingChain 模型映射链,如 "a→b→c"
|
||
ModelMappingChain *string
|
||
// BillingTier 计费层级标签(per_request/image 模式)
|
||
BillingTier *string
|
||
// BillingMode 计费模式:token/image
|
||
BillingMode *string
|
||
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
|
||
ServiceTier *string
|
||
// ReasoningEffort is the request's reasoning effort level.
|
||
// OpenAI: "low" / "medium" / "high" / "xhigh"; Claude: "low" / "medium" / "high" / "max".
|
||
// Nil means not provided / not applicable.
|
||
ReasoningEffort *string
|
||
// InboundEndpoint is the client-facing API endpoint path, e.g. /v1/chat/completions.
|
||
InboundEndpoint *string
|
||
// UpstreamEndpoint is the normalized upstream endpoint path, e.g. /v1/responses.
|
||
UpstreamEndpoint *string
|
||
|
||
GroupID *int64
|
||
SubscriptionID *int64
|
||
|
||
InputTokens int
|
||
OutputTokens int
|
||
CacheCreationTokens int
|
||
CacheReadTokens int
|
||
|
||
CacheCreation5mTokens int `gorm:"column:cache_creation_5m_tokens"`
|
||
CacheCreation1hTokens int `gorm:"column:cache_creation_1h_tokens"`
|
||
|
||
ImageOutputTokens int
|
||
ImageOutputCost float64
|
||
|
||
InputCost float64
|
||
OutputCost float64
|
||
CacheCreationCost float64
|
||
CacheReadCost float64
|
||
TotalCost float64
|
||
ActualCost float64
|
||
RateMultiplier float64
|
||
// AccountRateMultiplier 账号计费倍率快照(nil 表示历史数据,按 1.0 处理)
|
||
AccountRateMultiplier *float64
|
||
// AccountStatsCost 账号统计定价预计算费用(nil = 使用默认公式 total_cost × account_rate_multiplier)
|
||
AccountStatsCost *float64
|
||
|
||
BillingType int8
|
||
RequestType RequestType
|
||
Stream bool
|
||
OpenAIWSMode bool
|
||
DurationMs *int
|
||
FirstTokenMs *int
|
||
UserAgent *string
|
||
IPAddress *string
|
||
|
||
// Cache TTL Override 标记(管理员强制替换了缓存 TTL 计费)
|
||
CacheTTLOverridden bool
|
||
|
||
// 图片生成字段
|
||
ImageCount int
|
||
ImageSize *string
|
||
ImageInputSize *string
|
||
ImageOutputSize *string
|
||
ImageSizeSource *string
|
||
ImageSizeBreakdown map[string]int
|
||
MediaType *string
|
||
|
||
CreatedAt time.Time
|
||
|
||
User *User
|
||
APIKey *APIKey
|
||
Account *Account
|
||
Group *Group
|
||
Subscription *UserSubscription
|
||
}
|
||
|
||
func (u *UsageLog) TotalTokens() int {
|
||
return u.InputTokens + u.OutputTokens + u.CacheCreationTokens + u.CacheReadTokens
|
||
}
|
||
|
||
func (u *UsageLog) EffectiveRequestType() RequestType {
|
||
if u == nil {
|
||
return RequestTypeUnknown
|
||
}
|
||
if normalized := u.RequestType.Normalize(); normalized != RequestTypeUnknown {
|
||
return normalized
|
||
}
|
||
return RequestTypeFromLegacy(u.Stream, u.OpenAIWSMode)
|
||
}
|
||
|
||
func (u *UsageLog) SyncRequestTypeAndLegacyFields() {
|
||
if u == nil {
|
||
return
|
||
}
|
||
requestType := u.EffectiveRequestType()
|
||
u.RequestType = requestType
|
||
u.Stream, u.OpenAIWSMode = ApplyLegacyRequestFields(requestType, u.Stream, u.OpenAIWSMode)
|
||
}
|