Files
sub2api/backend/internal/service/usage_log.go
erio ebac0dc628 feat(channel): 缓存扁平化 + 网关映射集成 + 计费模式统一 + 模型限制
- 缓存重构为 O(1) 哈希结构 (pricingByGroupModel, mappingByGroupModel)
- 渠道模型映射接入网关流程 (Forward 前应用, a→b→c 映射链)
- 新增 billing_model_source 配置 (请求模型/最终模型计费)
- usage_logs 新增 channel_id, model_mapping_chain, billing_tier 字段
- 每种计费模式统一支持默认价格 + 区间定价
- 渠道模型限制开关 (restrict_models)
- 分组按平台分类展示 + 彩色图标
- 必填字段红色星号 + 模型映射 UI
- 去除模型通配符支持
2026-04-04 11:09:01 +08:00

193 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
)
func (t RequestType) IsValid() bool {
switch t {
case RequestTypeUnknown, RequestTypeSync, RequestTypeStream, RequestTypeWSV2:
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"
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
default:
return RequestTypeUnknown, fmt.Errorf("invalid request_type, allowed values: unknown, sync, stream, ws_v2")
}
}
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
// 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"`
InputCost float64
OutputCost float64
CacheCreationCost float64
CacheReadCost float64
TotalCost float64
ActualCost float64
RateMultiplier float64
// AccountRateMultiplier 账号计费倍率快照nil 表示历史数据,按 1.0 处理)
AccountRateMultiplier *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
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)
}