mappers.go 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007
  1. // Package dto provides data transfer objects for HTTP handlers.
  2. package dto
  3. import (
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. )
  9. func UserFromServiceShallow(u *service.User) *User {
  10. if u == nil {
  11. return nil
  12. }
  13. return &User{
  14. ID: u.ID,
  15. Email: u.Email,
  16. Username: u.Username,
  17. Role: u.Role,
  18. Balance: u.Balance,
  19. FrozenBalance: u.FrozenBalance,
  20. Concurrency: u.Concurrency,
  21. Status: u.Status,
  22. AllowedGroups: u.AllowedGroups,
  23. LastActiveAt: u.LastActiveAt,
  24. CreatedAt: u.CreatedAt,
  25. UpdatedAt: u.UpdatedAt,
  26. BalanceNotifyEnabled: u.BalanceNotifyEnabled,
  27. BalanceNotifyThresholdType: u.BalanceNotifyThresholdType,
  28. BalanceNotifyThreshold: u.BalanceNotifyThreshold,
  29. BalanceNotifyExtraEmails: NotifyEmailEntriesFromService(u.BalanceNotifyExtraEmails),
  30. TotalRecharged: u.TotalRecharged,
  31. RPMLimit: u.RPMLimit,
  32. DeletedAt: u.DeletedAt,
  33. }
  34. }
  35. func UserFromService(u *service.User) *User {
  36. if u == nil {
  37. return nil
  38. }
  39. out := UserFromServiceShallow(u)
  40. if len(u.APIKeys) > 0 {
  41. out.APIKeys = make([]APIKey, 0, len(u.APIKeys))
  42. for i := range u.APIKeys {
  43. k := u.APIKeys[i]
  44. out.APIKeys = append(out.APIKeys, *APIKeyFromService(&k))
  45. }
  46. }
  47. if len(u.Subscriptions) > 0 {
  48. out.Subscriptions = make([]UserSubscription, 0, len(u.Subscriptions))
  49. for i := range u.Subscriptions {
  50. s := u.Subscriptions[i]
  51. out.Subscriptions = append(out.Subscriptions, *UserSubscriptionFromService(&s))
  52. }
  53. }
  54. return out
  55. }
  56. // UserFromServiceAdmin converts a service User to DTO for admin users.
  57. // It includes notes - user-facing endpoints must not use this.
  58. func UserFromServiceAdmin(u *service.User) *AdminUser {
  59. if u == nil {
  60. return nil
  61. }
  62. base := UserFromService(u)
  63. if base == nil {
  64. return nil
  65. }
  66. return &AdminUser{
  67. User: *base,
  68. Notes: u.Notes,
  69. LastUsedAt: u.LastUsedAt,
  70. GroupRates: u.GroupRates,
  71. RestrictPublicGroups: u.RestrictPublicGroups,
  72. }
  73. }
  74. func APIKeyFromService(k *service.APIKey) *APIKey {
  75. if k == nil {
  76. return nil
  77. }
  78. out := &APIKey{
  79. ID: k.ID,
  80. UserID: k.UserID,
  81. Key: k.Key,
  82. Name: k.Name,
  83. GroupID: k.GroupID,
  84. Status: k.Status,
  85. IPWhitelist: k.IPWhitelist,
  86. IPBlacklist: k.IPBlacklist,
  87. LastUsedAt: k.LastUsedAt,
  88. LastUsedIP: k.LastUsedIP,
  89. Quota: k.Quota,
  90. QuotaUsed: k.QuotaUsed,
  91. ExpiresAt: k.ExpiresAt,
  92. CreatedAt: k.CreatedAt,
  93. UpdatedAt: k.UpdatedAt,
  94. CurrentConcurrency: k.CurrentConcurrency,
  95. RateLimit5h: k.RateLimit5h,
  96. RateLimit1d: k.RateLimit1d,
  97. RateLimit7d: k.RateLimit7d,
  98. Usage5h: k.EffectiveUsage5h(),
  99. Usage1d: k.EffectiveUsage1d(),
  100. Usage7d: k.EffectiveUsage7d(),
  101. Window5hStart: k.Window5hStart,
  102. Window1dStart: k.Window1dStart,
  103. Window7dStart: k.Window7dStart,
  104. User: UserFromServiceShallow(k.User),
  105. Group: GroupFromServiceShallow(k.Group),
  106. }
  107. if k.Window5hStart != nil && !service.IsWindowExpired(k.Window5hStart, service.RateLimitWindow5h) {
  108. t := k.Window5hStart.Add(service.RateLimitWindow5h)
  109. out.Reset5hAt = &t
  110. }
  111. if k.Window1dStart != nil && !service.IsWindowExpired(k.Window1dStart, service.RateLimitWindow1d) {
  112. t := k.Window1dStart.Add(service.RateLimitWindow1d)
  113. out.Reset1dAt = &t
  114. }
  115. if k.Window7dStart != nil && !service.IsWindowExpired(k.Window7dStart, service.RateLimitWindow7d) {
  116. t := k.Window7dStart.Add(service.RateLimitWindow7d)
  117. out.Reset7dAt = &t
  118. }
  119. if k.Key == "" && k.KeyHash != "" {
  120. out.KeyDisplay = k.KeyPrefix + "********"
  121. }
  122. return out
  123. }
  124. func GroupFromServiceShallow(g *service.Group) *Group {
  125. if g == nil {
  126. return nil
  127. }
  128. out := groupFromServiceBase(g)
  129. return &out
  130. }
  131. func GroupFromService(g *service.Group) *Group {
  132. if g == nil {
  133. return nil
  134. }
  135. return GroupFromServiceShallow(g)
  136. }
  137. // GroupFromServiceAdmin converts a service Group to DTO for admin users.
  138. // It includes internal fields like model_routing and account_count.
  139. func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
  140. if g == nil {
  141. return nil
  142. }
  143. out := &AdminGroup{
  144. Group: groupFromServiceBase(g),
  145. ForceOpenAIFast: g.ForceOpenAIFast,
  146. FreeOpenAIFast: g.FreeOpenAIFast,
  147. ProfitControlEnabled: g.ProfitControlEnabled,
  148. ProfitMinMargin: g.ProfitMinMargin,
  149. ProfitSafetyBuffer: g.ProfitSafetyBuffer,
  150. ModelPricing: g.ModelPricing,
  151. ModelRouting: g.ModelRouting,
  152. ModelRoutingEnabled: g.ModelRoutingEnabled,
  153. MCPXMLInject: g.MCPXMLInject,
  154. DefaultMappedModel: g.DefaultMappedModel,
  155. MessagesDispatchModelConfig: g.MessagesDispatchModelConfig,
  156. ModelAllowlist: g.ModelAllowlist,
  157. CodexModelsManifestConfig: g.CodexModelsManifestConfig,
  158. SupportedModelScopes: g.SupportedModelScopes,
  159. AccountCount: g.AccountCount,
  160. ActiveAccountCount: g.ActiveAccountCount,
  161. RateLimitedAccountCount: g.RateLimitedAccountCount,
  162. SortOrder: g.SortOrder,
  163. }
  164. if len(g.AccountGroups) > 0 {
  165. out.AccountGroups = make([]AccountGroup, 0, len(g.AccountGroups))
  166. for i := range g.AccountGroups {
  167. ag := g.AccountGroups[i]
  168. out.AccountGroups = append(out.AccountGroups, *AccountGroupFromService(&ag))
  169. }
  170. }
  171. return out
  172. }
  173. func groupFromServiceBase(g *service.Group) Group {
  174. return Group{
  175. ID: g.ID,
  176. Name: g.Name,
  177. Description: g.Description,
  178. Platform: g.Platform,
  179. RateMultiplier: g.RateMultiplier,
  180. IsExclusive: g.IsExclusive,
  181. SecurityPolicyEnabled: g.SecurityPolicyEnabled,
  182. SecurityPolicyMode: g.SecurityPolicyMode,
  183. SecurityPolicyEmailEnabled: g.SecurityPolicyEmailEnabled,
  184. Status: g.Status,
  185. SubscriptionType: g.SubscriptionType,
  186. DailyLimitUSD: g.DailyLimitUSD,
  187. WeeklyLimitUSD: g.WeeklyLimitUSD,
  188. MonthlyLimitUSD: g.MonthlyLimitUSD,
  189. LongContextPricingEnabled: g.LongContextPricingEnabled,
  190. AllowImageGeneration: g.AllowImageGeneration,
  191. AllowBatchImageGeneration: g.AllowBatchImageGeneration,
  192. ImageRateIndependent: g.ImageRateIndependent,
  193. ImageRateMultiplier: g.ImageRateMultiplier,
  194. BatchImageDiscountMultiplier: g.BatchImageDiscountMultiplier,
  195. BatchImageHoldMultiplier: g.BatchImageHoldMultiplier,
  196. VideoRateIndependent: g.VideoRateIndependent,
  197. VideoRateMultiplier: g.VideoRateMultiplier,
  198. PeakRateEnabled: g.PeakRateEnabled,
  199. PeakStart: g.PeakStart,
  200. PeakEnd: g.PeakEnd,
  201. PeakRateMultiplier: g.PeakRateMultiplier,
  202. ImagePrice1K: g.ImagePrice1K,
  203. ImagePrice2K: g.ImagePrice2K,
  204. ImagePrice4K: g.ImagePrice4K,
  205. VideoPrice480P: g.VideoPrice480P,
  206. VideoPrice720P: g.VideoPrice720P,
  207. VideoPrice1080P: g.VideoPrice1080P,
  208. VideoModelPrices: g.VideoModelPrices,
  209. WebSearchPricePerCall: g.WebSearchPricePerCall,
  210. SearchPricePer1k: g.SearchPricePer1k,
  211. AudioRealtimePricePerMin: g.AudioRealtimePricePerMin,
  212. AudioTtsPricePerMillionChars: g.AudioTTSPricePerMillionChars,
  213. AudioSttPricePerHour: g.AudioSTTPricePerHour,
  214. ClaudeCodeOnly: g.ClaudeCodeOnly,
  215. FallbackGroupID: g.FallbackGroupID,
  216. FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest,
  217. AllowMessagesDispatch: g.AllowMessagesDispatch,
  218. AllowLive: g.AllowLive,
  219. RequireOAuthOnly: g.RequireOAuthOnly,
  220. RequirePrivacySet: g.RequirePrivacySet,
  221. RPMLimit: g.RPMLimit,
  222. MaxReasoningEffort: g.MaxReasoningEffort,
  223. MaxReasoningEffortOverLimit: g.MaxReasoningEffortOverLimit,
  224. ReasoningEffortMappings: g.ReasoningEffortMappings,
  225. CreatedAt: g.CreatedAt,
  226. UpdatedAt: g.UpdatedAt,
  227. }
  228. }
  229. func AccountFromServiceShallow(a *service.Account) *Account {
  230. if a == nil {
  231. return nil
  232. }
  233. redactedCreds, credsStatus := RedactCredentials(a.Credentials)
  234. extra := redactAccountManagedExtra(a.Extra)
  235. var ollamaCloudUsage *service.OllamaCloudUsageState
  236. if state := service.OllamaCloudUsageStateFromAccount(a); state.Eligible {
  237. ollamaCloudUsage = state
  238. }
  239. out := &Account{
  240. AntiDegradation: a.AntiDegradationEnabled(),
  241. ProtectionScope: a.ProtectionScope(),
  242. ProtectionMode: a.ProtectionMode(),
  243. ID: a.ID,
  244. Name: a.Name,
  245. Notes: a.Notes,
  246. Platform: a.Platform,
  247. Type: a.Type,
  248. Credentials: redactedCreds,
  249. CredentialsStatus: credsStatus,
  250. Extra: extra,
  251. OllamaCloudUsage: ollamaCloudUsage,
  252. ProxyID: a.ProxyID,
  253. ProxyFallbackOriginID: a.ProxyFallbackOriginID,
  254. ProxyFallbackOriginName: a.ProxyFallbackOriginName,
  255. Concurrency: a.Concurrency,
  256. LoadFactor: a.LoadFactor,
  257. Priority: a.Priority,
  258. RateMultiplier: a.BillingRateMultiplier(),
  259. Status: a.Status,
  260. ErrorMessage: a.ErrorMessage,
  261. LastUsedAt: a.LastUsedAt,
  262. ExpiresAt: timeToUnixSeconds(a.ExpiresAt),
  263. AutoPauseOnExpired: a.AutoPauseOnExpired,
  264. CreatedAt: a.CreatedAt,
  265. UpdatedAt: a.UpdatedAt,
  266. Schedulable: a.Schedulable,
  267. RateLimitedAt: a.RateLimitedAt,
  268. RateLimitResetAt: a.RateLimitResetAt,
  269. OverloadUntil: a.OverloadUntil,
  270. TempUnschedulableUntil: a.TempUnschedulableUntil,
  271. TempUnschedulableReason: a.TempUnschedulableReason,
  272. SessionWindowStart: a.SessionWindowStart,
  273. SessionWindowEnd: a.SessionWindowEnd,
  274. SessionWindowStatus: a.SessionWindowStatus,
  275. GroupIDs: a.GroupIDs,
  276. ParentAccountID: a.ParentAccountID,
  277. QuotaDimension: a.QuotaDimension,
  278. }
  279. // 提取 5h 窗口费用控制和会话数量控制配置(仅 Anthropic OAuth/SetupToken 账号有效)
  280. if a.IsAnthropicOAuthOrSetupToken() {
  281. if limit := a.GetWindowCostLimit(); limit > 0 {
  282. out.WindowCostLimit = &limit
  283. }
  284. if reserve := a.GetWindowCostStickyReserve(); reserve > 0 {
  285. out.WindowCostStickyReserve = &reserve
  286. }
  287. if maxSessions := a.GetMaxSessions(); maxSessions > 0 {
  288. out.MaxSessions = &maxSessions
  289. }
  290. if idleTimeout := a.GetSessionIdleTimeoutMinutes(); idleTimeout > 0 {
  291. out.SessionIdleTimeoutMin = &idleTimeout
  292. }
  293. if rpm := a.GetBaseRPM(); rpm > 0 {
  294. out.BaseRPM = &rpm
  295. strategy := a.GetRPMStrategy()
  296. out.RPMStrategy = &strategy
  297. buffer := a.GetRPMStickyBuffer()
  298. out.RPMStickyBuffer = &buffer
  299. }
  300. // 用户消息队列模式
  301. if mode := a.GetUserMsgQueueMode(); mode != "" {
  302. out.UserMsgQueueMode = &mode
  303. }
  304. // TLS指纹伪装开关
  305. if a.IsTLSFingerprintEnabled() {
  306. enabled := true
  307. out.EnableTLSFingerprint = &enabled
  308. }
  309. // TLS指纹模板ID
  310. if profileID := a.GetTLSFingerprintProfileID(); profileID > 0 {
  311. out.TLSFingerprintProfileID = &profileID
  312. }
  313. // 会话ID伪装开关
  314. if a.IsSessionIDMaskingEnabled() {
  315. enabled := true
  316. out.EnableSessionIDMasking = &enabled
  317. }
  318. // 缓存 TTL 强制替换
  319. if a.IsCacheTTLOverrideEnabled() {
  320. enabled := true
  321. out.CacheTTLOverrideEnabled = &enabled
  322. target := a.GetCacheTTLOverrideTarget()
  323. out.CacheTTLOverrideTarget = &target
  324. }
  325. // 自定义 Base URL 中继转发
  326. if a.IsCustomBaseURLEnabled() {
  327. enabled := true
  328. out.CustomBaseURLEnabled = &enabled
  329. if customURL := a.GetCustomBaseURL(); customURL != "" {
  330. out.CustomBaseURL = &customURL
  331. }
  332. }
  333. }
  334. // 提取账号配额限制(apikey / bedrock 类型有效)
  335. if a.IsAPIKeyOrBedrock() {
  336. if limit := a.GetQuotaLimit(); limit > 0 {
  337. out.QuotaLimit = &limit
  338. used := a.GetQuotaUsed()
  339. out.QuotaUsed = &used
  340. }
  341. if limit := a.GetQuotaDailyLimit(); limit > 0 {
  342. out.QuotaDailyLimit = &limit
  343. used := a.GetQuotaDailyUsed()
  344. if a.IsDailyQuotaPeriodExpired() {
  345. used = 0
  346. }
  347. out.QuotaDailyUsed = &used
  348. }
  349. if limit := a.GetQuotaWeeklyLimit(); limit > 0 {
  350. out.QuotaWeeklyLimit = &limit
  351. used := a.GetQuotaWeeklyUsed()
  352. if a.IsWeeklyQuotaPeriodExpired() {
  353. used = 0
  354. }
  355. out.QuotaWeeklyUsed = &used
  356. }
  357. // 固定时间重置配置
  358. if mode := a.GetQuotaDailyResetMode(); mode == "fixed" {
  359. out.QuotaDailyResetMode = &mode
  360. hour := a.GetQuotaDailyResetHour()
  361. out.QuotaDailyResetHour = &hour
  362. }
  363. if mode := a.GetQuotaWeeklyResetMode(); mode == "fixed" {
  364. out.QuotaWeeklyResetMode = &mode
  365. day := a.GetQuotaWeeklyResetDay()
  366. out.QuotaWeeklyResetDay = &day
  367. hour := a.GetQuotaWeeklyResetHour()
  368. out.QuotaWeeklyResetHour = &hour
  369. }
  370. if a.GetQuotaDailyResetMode() == "fixed" || a.GetQuotaWeeklyResetMode() == "fixed" {
  371. tz := a.GetQuotaResetTimezone()
  372. out.QuotaResetTimezone = &tz
  373. }
  374. if a.Extra != nil {
  375. if v, ok := a.Extra["quota_daily_reset_at"].(string); ok && v != "" {
  376. out.QuotaDailyResetAt = &v
  377. }
  378. if v, ok := a.Extra["quota_weekly_reset_at"].(string); ok && v != "" {
  379. out.QuotaWeeklyResetAt = &v
  380. }
  381. }
  382. // 配额通知配置
  383. if enabled := a.GetQuotaNotifyDailyEnabled(); enabled {
  384. out.QuotaNotifyDailyEnabled = &enabled
  385. }
  386. if threshold := a.GetQuotaNotifyDailyThreshold(); threshold > 0 {
  387. out.QuotaNotifyDailyThreshold = &threshold
  388. }
  389. if enabled := a.GetQuotaNotifyWeeklyEnabled(); enabled {
  390. out.QuotaNotifyWeeklyEnabled = &enabled
  391. }
  392. if threshold := a.GetQuotaNotifyWeeklyThreshold(); threshold > 0 {
  393. out.QuotaNotifyWeeklyThreshold = &threshold
  394. }
  395. if enabled := a.GetQuotaNotifyTotalEnabled(); enabled {
  396. out.QuotaNotifyTotalEnabled = &enabled
  397. }
  398. if threshold := a.GetQuotaNotifyTotalThreshold(); threshold > 0 {
  399. out.QuotaNotifyTotalThreshold = &threshold
  400. }
  401. }
  402. return out
  403. }
  404. func redactAccountManagedExtra(extra map[string]any) map[string]any {
  405. if extra == nil {
  406. return nil
  407. }
  408. redacted := make(map[string]any, len(extra))
  409. for key, value := range extra {
  410. switch key {
  411. case "codex_fingerprint_seed":
  412. continue
  413. case service.OllamaCloudUsageSessionExtraKey,
  414. service.OllamaCloudUsageAutoRefreshExtraKey,
  415. service.OllamaCloudUsageSnapshotExtraKey:
  416. continue
  417. default:
  418. redacted[key] = value
  419. }
  420. }
  421. return redacted
  422. }
  423. func AccountFromService(a *service.Account) *Account {
  424. if a == nil {
  425. return nil
  426. }
  427. out := AccountFromServiceShallow(a)
  428. out.Proxy = ProxyFromService(a.Proxy)
  429. if len(a.AccountGroups) > 0 {
  430. out.AccountGroups = make([]AccountGroup, 0, len(a.AccountGroups))
  431. for i := range a.AccountGroups {
  432. ag := a.AccountGroups[i]
  433. out.AccountGroups = append(out.AccountGroups, *AccountGroupFromService(&ag))
  434. }
  435. }
  436. if len(a.Groups) > 0 {
  437. out.Groups = make([]*Group, 0, len(a.Groups))
  438. for _, g := range a.Groups {
  439. out.Groups = append(out.Groups, GroupFromServiceShallow(g))
  440. }
  441. }
  442. return out
  443. }
  444. // AccountListItemFromAccount projects a full account response into the
  445. // compact shape used by the paginated admin account list. Keeping this
  446. // projection separate from Account preserves the existing detail API.
  447. func AccountListItemFromAccount(a *Account) *AccountListItem {
  448. if a == nil {
  449. return nil
  450. }
  451. return &AccountListItem{
  452. AntiDegradation: a.AntiDegradation,
  453. ProtectionScope: a.ProtectionScope,
  454. ProtectionMode: a.ProtectionMode,
  455. ID: a.ID, Name: a.Name, Notes: a.Notes, Platform: a.Platform, Type: a.Type,
  456. Credentials: a.Credentials, CredentialsStatus: a.CredentialsStatus, Extra: a.Extra,
  457. OllamaCloudUsage: a.OllamaCloudUsage,
  458. ProxyID: a.ProxyID, ProxyFallbackOriginID: a.ProxyFallbackOriginID, ProxyFallbackOriginName: a.ProxyFallbackOriginName,
  459. Concurrency: a.Concurrency, LoadFactor: a.LoadFactor, Priority: a.Priority, RateMultiplier: a.RateMultiplier,
  460. Status: a.Status, ErrorMessage: a.ErrorMessage, LastUsedAt: a.LastUsedAt, ExpiresAt: a.ExpiresAt,
  461. AutoPauseOnExpired: a.AutoPauseOnExpired, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt,
  462. Schedulable: a.Schedulable, RateLimitedAt: a.RateLimitedAt, RateLimitResetAt: a.RateLimitResetAt,
  463. OverloadUntil: a.OverloadUntil, TempUnschedulableUntil: a.TempUnschedulableUntil,
  464. TempUnschedulableReason: a.TempUnschedulableReason, SessionWindowStart: a.SessionWindowStart,
  465. SessionWindowEnd: a.SessionWindowEnd, SessionWindowStatus: a.SessionWindowStatus,
  466. WindowCostLimit: a.WindowCostLimit, WindowCostStickyReserve: a.WindowCostStickyReserve,
  467. MaxSessions: a.MaxSessions, SessionIdleTimeoutMin: a.SessionIdleTimeoutMin, BaseRPM: a.BaseRPM,
  468. RPMStrategy: a.RPMStrategy, RPMStickyBuffer: a.RPMStickyBuffer, UserMsgQueueMode: a.UserMsgQueueMode,
  469. EnableTLSFingerprint: a.EnableTLSFingerprint, TLSFingerprintProfileID: a.TLSFingerprintProfileID,
  470. EnableSessionIDMasking: a.EnableSessionIDMasking, CacheTTLOverrideEnabled: a.CacheTTLOverrideEnabled,
  471. CacheTTLOverrideTarget: a.CacheTTLOverrideTarget, CustomBaseURLEnabled: a.CustomBaseURLEnabled,
  472. CustomBaseURL: a.CustomBaseURL, QuotaLimit: a.QuotaLimit, QuotaUsed: a.QuotaUsed,
  473. QuotaDailyLimit: a.QuotaDailyLimit, QuotaDailyUsed: a.QuotaDailyUsed, QuotaWeeklyLimit: a.QuotaWeeklyLimit,
  474. QuotaWeeklyUsed: a.QuotaWeeklyUsed, QuotaDailyResetMode: a.QuotaDailyResetMode,
  475. QuotaDailyResetHour: a.QuotaDailyResetHour, QuotaWeeklyResetMode: a.QuotaWeeklyResetMode,
  476. QuotaWeeklyResetDay: a.QuotaWeeklyResetDay, QuotaWeeklyResetHour: a.QuotaWeeklyResetHour,
  477. QuotaResetTimezone: a.QuotaResetTimezone, QuotaDailyResetAt: a.QuotaDailyResetAt,
  478. QuotaWeeklyResetAt: a.QuotaWeeklyResetAt, QuotaNotifyDailyEnabled: a.QuotaNotifyDailyEnabled,
  479. QuotaNotifyDailyThreshold: a.QuotaNotifyDailyThreshold, QuotaNotifyWeeklyEnabled: a.QuotaNotifyWeeklyEnabled,
  480. QuotaNotifyWeeklyThreshold: a.QuotaNotifyWeeklyThreshold, QuotaNotifyTotalEnabled: a.QuotaNotifyTotalEnabled,
  481. QuotaNotifyTotalThreshold: a.QuotaNotifyTotalThreshold, ParentAccountID: a.ParentAccountID,
  482. QuotaDimension: a.QuotaDimension, ParentEmail: a.ParentEmail, ParentPlanType: a.ParentPlanType,
  483. ParentPrivacyMode: a.ParentPrivacyMode, ParentSubscriptionExpiresAt: a.ParentSubscriptionExpiresAt,
  484. ParentChatGPTAccountID: a.ParentChatGPTAccountID, Proxy: a.Proxy, GroupIDs: a.GroupIDs,
  485. }
  486. }
  487. func timeToUnixSeconds(value *time.Time) *int64 {
  488. if value == nil {
  489. return nil
  490. }
  491. ts := value.Unix()
  492. return &ts
  493. }
  494. func AccountGroupFromService(ag *service.AccountGroup) *AccountGroup {
  495. if ag == nil {
  496. return nil
  497. }
  498. return &AccountGroup{
  499. AccountID: ag.AccountID,
  500. GroupID: ag.GroupID,
  501. Priority: ag.Priority,
  502. CreatedAt: ag.CreatedAt,
  503. Account: AccountFromServiceShallow(ag.Account),
  504. Group: GroupFromServiceShallow(ag.Group),
  505. }
  506. }
  507. func ProxyFromService(p *service.Proxy) *Proxy {
  508. if p == nil {
  509. return nil
  510. }
  511. return &Proxy{
  512. ID: p.ID,
  513. Name: p.Name,
  514. Protocol: p.Protocol,
  515. Host: p.Host,
  516. Port: p.Port,
  517. Username: p.Username,
  518. Status: p.Status,
  519. CreatedAt: p.CreatedAt,
  520. UpdatedAt: p.UpdatedAt,
  521. ExpiresAt: p.ExpiresAt,
  522. FallbackMode: p.FallbackMode,
  523. BackupProxyID: p.BackupProxyID,
  524. ExpiryWarnDays: p.ExpiryWarnDays,
  525. }
  526. }
  527. func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWithAccountCount {
  528. if p == nil {
  529. return nil
  530. }
  531. return &ProxyWithAccountCount{
  532. Proxy: *ProxyFromService(&p.Proxy),
  533. AccountCount: p.AccountCount,
  534. LatencyMs: p.LatencyMs,
  535. LatencyStatus: p.LatencyStatus,
  536. LatencyMessage: p.LatencyMessage,
  537. IPAddress: p.IPAddress,
  538. Country: p.Country,
  539. CountryCode: p.CountryCode,
  540. Region: p.Region,
  541. City: p.City,
  542. QualityStatus: p.QualityStatus,
  543. QualityScore: p.QualityScore,
  544. QualityGrade: p.QualityGrade,
  545. QualitySummary: p.QualitySummary,
  546. QualityChecked: p.QualityChecked,
  547. }
  548. }
  549. // ProxyFromServiceAdmin converts a service Proxy to AdminProxy DTO for admin users.
  550. // It includes the password field - user-facing endpoints must not use this.
  551. func ProxyFromServiceAdmin(p *service.Proxy) *AdminProxy {
  552. if p == nil {
  553. return nil
  554. }
  555. base := ProxyFromService(p)
  556. if base == nil {
  557. return nil
  558. }
  559. return &AdminProxy{
  560. Proxy: *base,
  561. Password: p.Password,
  562. }
  563. }
  564. // ProxyWithAccountCountFromServiceAdmin converts a service ProxyWithAccountCount to AdminProxyWithAccountCount DTO.
  565. // It includes the password field - user-facing endpoints must not use this.
  566. func ProxyWithAccountCountFromServiceAdmin(p *service.ProxyWithAccountCount) *AdminProxyWithAccountCount {
  567. if p == nil {
  568. return nil
  569. }
  570. admin := ProxyFromServiceAdmin(&p.Proxy)
  571. if admin == nil {
  572. return nil
  573. }
  574. return &AdminProxyWithAccountCount{
  575. AdminProxy: *admin,
  576. AccountCount: p.AccountCount,
  577. LatencyMs: p.LatencyMs,
  578. LatencyStatus: p.LatencyStatus,
  579. LatencyMessage: p.LatencyMessage,
  580. IPAddress: p.IPAddress,
  581. Country: p.Country,
  582. CountryCode: p.CountryCode,
  583. Region: p.Region,
  584. City: p.City,
  585. QualityStatus: p.QualityStatus,
  586. QualityScore: p.QualityScore,
  587. QualityGrade: p.QualityGrade,
  588. QualitySummary: p.QualitySummary,
  589. QualityChecked: p.QualityChecked,
  590. }
  591. }
  592. func ProxyAccountSummaryFromService(a *service.ProxyAccountSummary) *ProxyAccountSummary {
  593. if a == nil {
  594. return nil
  595. }
  596. return &ProxyAccountSummary{
  597. ID: a.ID,
  598. Name: a.Name,
  599. Platform: a.Platform,
  600. Type: a.Type,
  601. Notes: a.Notes,
  602. }
  603. }
  604. func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
  605. if rc == nil {
  606. return nil
  607. }
  608. out := redeemCodeFromServiceBase(rc)
  609. return &out
  610. }
  611. // RedeemCodeFromServiceAdmin converts a service RedeemCode to DTO for admin users.
  612. // It includes notes - user-facing endpoints must not use this.
  613. func RedeemCodeFromServiceAdmin(rc *service.RedeemCode) *AdminRedeemCode {
  614. if rc == nil {
  615. return nil
  616. }
  617. return &AdminRedeemCode{
  618. RedeemCode: redeemCodeFromServiceBase(rc),
  619. Notes: rc.Notes,
  620. }
  621. }
  622. func redeemCodeFromServiceBase(rc *service.RedeemCode) RedeemCode {
  623. out := RedeemCode{
  624. ID: rc.ID,
  625. Code: rc.Code,
  626. Type: rc.Type,
  627. Value: rc.Value,
  628. Status: rc.Status,
  629. UsedBy: rc.UsedBy,
  630. UsedAt: rc.UsedAt,
  631. CreatedAt: rc.CreatedAt,
  632. ExpiresAt: rc.ExpiresAt,
  633. GroupID: rc.GroupID,
  634. ValidityDays: rc.ValidityDays,
  635. User: UserFromServiceShallow(rc.User),
  636. Group: GroupFromServiceShallow(rc.Group),
  637. }
  638. if rc.IsExpired() {
  639. out.Status = service.StatusExpired
  640. }
  641. // For admin_balance/admin_concurrency types, include notes so users can see
  642. // why they were charged or credited by admin
  643. if (rc.Type == "admin_balance" || rc.Type == "admin_concurrency") && rc.Notes != "" {
  644. out.Notes = &rc.Notes
  645. }
  646. return out
  647. }
  648. // AccountSummaryFromService returns a minimal AccountSummary for usage log display.
  649. // Only includes ID and Name - no sensitive fields like Credentials, Proxy, etc.
  650. func AccountSummaryFromService(a *service.Account) *AccountSummary {
  651. if a == nil {
  652. return nil
  653. }
  654. return &AccountSummary{
  655. ID: a.ID,
  656. Name: a.Name,
  657. }
  658. }
  659. func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
  660. // 普通用户 DTO:严禁包含管理员字段(例如 account_rate_multiplier、account、upstream_model)。
  661. requestType := l.EffectiveRequestType()
  662. stream, openAIWSMode := service.ApplyLegacyRequestFields(requestType, l.Stream, l.OpenAIWSMode)
  663. requestedModel := l.RequestedModel
  664. if requestedModel == "" {
  665. requestedModel = l.Model
  666. }
  667. return UsageLog{
  668. ID: l.ID,
  669. UserID: l.UserID,
  670. APIKeyID: l.APIKeyID,
  671. AccountID: l.AccountID,
  672. RequestID: l.RequestID,
  673. Model: requestedModel,
  674. ServiceTier: l.ServiceTier,
  675. ReasoningEffort: userFacingReasoningEffort(l),
  676. InboundEndpoint: l.InboundEndpoint,
  677. GroupID: l.GroupID,
  678. SubscriptionID: l.SubscriptionID,
  679. InputTokens: l.InputTokens,
  680. OutputTokens: l.OutputTokens,
  681. CacheCreationTokens: l.CacheCreationTokens,
  682. CacheReadTokens: l.CacheReadTokens,
  683. CacheCreation5mTokens: l.CacheCreation5mTokens,
  684. CacheCreation1hTokens: l.CacheCreation1hTokens,
  685. InputCost: l.InputCost,
  686. OutputCost: l.OutputCost,
  687. CacheCreationCost: l.CacheCreationCost,
  688. CacheReadCost: l.CacheReadCost,
  689. TotalCost: l.TotalCost,
  690. ActualCost: l.ActualCost,
  691. RateMultiplier: l.RateMultiplier,
  692. LongContextBillingApplied: l.LongContextBillingApplied,
  693. BillingType: l.BillingType,
  694. RequestType: requestType.String(),
  695. Stream: stream,
  696. OpenAIWSMode: openAIWSMode,
  697. NativeCompactionV2: l.NativeCompactionV2,
  698. DurationMs: l.DurationMs,
  699. FirstTokenMs: l.FirstTokenMs,
  700. ImageCount: l.ImageCount,
  701. ImageSize: l.ImageSize,
  702. ImageInputSize: l.ImageInputSize,
  703. ImageOutputSize: l.ImageOutputSize,
  704. ImageInputTokens: l.ImageInputTokens,
  705. ImageInputCost: l.ImageInputCost,
  706. ImageOutputTokens: l.ImageOutputTokens,
  707. ImageOutputCost: l.ImageOutputCost,
  708. ImageSizeSource: l.ImageSizeSource,
  709. ImageSizeBreakdown: l.ImageSizeBreakdown,
  710. MediaType: l.MediaType,
  711. UserAgent: l.UserAgent,
  712. IPAddress: l.IPAddress,
  713. SessionID: l.SessionID,
  714. CacheTTLOverridden: l.CacheTTLOverridden,
  715. BillingMode: l.BillingMode,
  716. CreatedAt: l.CreatedAt,
  717. User: UserFromServiceShallow(l.User),
  718. APIKey: APIKeyFromService(l.APIKey),
  719. Group: GroupFromServiceShallow(l.Group),
  720. Subscription: UserSubscriptionFromService(l.Subscription),
  721. }
  722. }
  723. // UsageLogFromService converts a service UsageLog to DTO for regular users.
  724. // It excludes admin-only account/upstream internals while keeping user billing and request metadata.
  725. func UsageLogFromService(l *service.UsageLog) *UsageLog {
  726. if l == nil {
  727. return nil
  728. }
  729. u := usageLogFromServiceUser(l)
  730. return &u
  731. }
  732. // UsageLogFromServiceAdmin converts a service UsageLog to DTO for admin users.
  733. // It includes minimal Account info (ID, Name only) and IP address.
  734. func UsageLogFromServiceAdmin(l *service.UsageLog) *AdminUsageLog {
  735. if l == nil {
  736. return nil
  737. }
  738. usageLog := usageLogFromServiceUser(l)
  739. usageLog.UpstreamEndpoint = l.UpstreamEndpoint
  740. return &AdminUsageLog{
  741. UsageLog: usageLog,
  742. UpstreamModel: l.UpstreamModel,
  743. UpstreamReasoningEffort: adminUpstreamReasoningEffort(l),
  744. UpstreamResponseModel: l.UpstreamResponseModel,
  745. UpstreamModelMismatch: l.UpstreamModelMismatch,
  746. ChannelID: l.ChannelID,
  747. ModelMappingChain: l.ModelMappingChain,
  748. UpstreamRequestID: l.UpstreamRequestID,
  749. BillingTier: l.BillingTier,
  750. AccountRateMultiplier: l.AccountRateMultiplier,
  751. AccountStatsCost: l.AccountStatsCost,
  752. IPAddress: l.IPAddress,
  753. Account: AccountSummaryFromService(l.Account),
  754. }
  755. }
  756. func userFacingReasoningEffort(l *service.UsageLog) *string {
  757. if l == nil {
  758. return nil
  759. }
  760. if requested := strings.TrimSpace(derefString(l.RequestedReasoningEffort)); requested != "" {
  761. return &requested
  762. }
  763. return l.ReasoningEffort
  764. }
  765. func adminUpstreamReasoningEffort(l *service.UsageLog) *string {
  766. if l == nil {
  767. return nil
  768. }
  769. forwarded := strings.TrimSpace(derefString(l.ReasoningEffort))
  770. if forwarded == "" {
  771. return nil
  772. }
  773. requested := userFacingReasoningEffort(l)
  774. if requested != nil && service.NormalizeMaxReasoningEffort(*requested) == service.NormalizeMaxReasoningEffort(forwarded) {
  775. return nil
  776. }
  777. return &forwarded
  778. }
  779. func derefString(value *string) string {
  780. if value == nil {
  781. return ""
  782. }
  783. return *value
  784. }
  785. func UsageCleanupTaskFromService(task *service.UsageCleanupTask) *UsageCleanupTask {
  786. if task == nil {
  787. return nil
  788. }
  789. return &UsageCleanupTask{
  790. ID: task.ID,
  791. Status: task.Status,
  792. Filters: UsageCleanupFilters{
  793. StartTime: task.Filters.StartTime,
  794. EndTime: task.Filters.EndTime,
  795. UserID: task.Filters.UserID,
  796. APIKeyID: task.Filters.APIKeyID,
  797. AccountID: task.Filters.AccountID,
  798. GroupID: task.Filters.GroupID,
  799. Model: task.Filters.Model,
  800. RequestType: requestTypeStringPtr(task.Filters.RequestType),
  801. Stream: task.Filters.Stream,
  802. BillingType: task.Filters.BillingType,
  803. },
  804. CreatedBy: task.CreatedBy,
  805. DeletedRows: task.DeletedRows,
  806. ErrorMessage: task.ErrorMsg,
  807. CanceledBy: task.CanceledBy,
  808. CanceledAt: task.CanceledAt,
  809. StartedAt: task.StartedAt,
  810. FinishedAt: task.FinishedAt,
  811. CreatedAt: task.CreatedAt,
  812. UpdatedAt: task.UpdatedAt,
  813. }
  814. }
  815. func requestTypeStringPtr(requestType *int16) *string {
  816. if requestType == nil {
  817. return nil
  818. }
  819. value := service.RequestTypeFromInt16(*requestType).String()
  820. return &value
  821. }
  822. func SettingFromService(s *service.Setting) *Setting {
  823. if s == nil {
  824. return nil
  825. }
  826. return &Setting{
  827. ID: s.ID,
  828. Key: s.Key,
  829. Value: s.Value,
  830. UpdatedAt: s.UpdatedAt,
  831. }
  832. }
  833. func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscription {
  834. if sub == nil {
  835. return nil
  836. }
  837. out := userSubscriptionFromServiceBase(sub)
  838. return &out
  839. }
  840. // UserSubscriptionFromServiceAdmin converts a service UserSubscription to DTO for admin users.
  841. // It includes assignment metadata and notes.
  842. func UserSubscriptionFromServiceAdmin(sub *service.UserSubscription) *AdminUserSubscription {
  843. if sub == nil {
  844. return nil
  845. }
  846. return &AdminUserSubscription{
  847. UserSubscription: userSubscriptionFromServiceBase(sub),
  848. AssignedBy: sub.AssignedBy,
  849. AssignedAt: sub.AssignedAt,
  850. Notes: sub.Notes,
  851. AssignedByUser: UserFromServiceShallow(sub.AssignedByUser),
  852. }
  853. }
  854. func userSubscriptionFromServiceBase(sub *service.UserSubscription) UserSubscription {
  855. return UserSubscription{
  856. ID: sub.ID,
  857. UserID: sub.UserID,
  858. GroupID: sub.GroupID,
  859. StartsAt: sub.StartsAt,
  860. ExpiresAt: sub.ExpiresAt,
  861. Status: sub.Status,
  862. DailyWindowStart: sub.DailyWindowStart,
  863. WeeklyWindowStart: sub.WeeklyWindowStart,
  864. MonthlyWindowStart: sub.MonthlyWindowStart,
  865. DailyUsageUSD: sub.DailyUsageUSD,
  866. WeeklyUsageUSD: sub.WeeklyUsageUSD,
  867. MonthlyUsageUSD: sub.MonthlyUsageUSD,
  868. CreatedAt: sub.CreatedAt,
  869. UpdatedAt: sub.UpdatedAt,
  870. RevokedAt: sub.DeletedAt,
  871. User: UserFromServiceShallow(sub.User),
  872. Group: GroupFromServiceShallow(sub.Group),
  873. }
  874. }
  875. func BulkAssignResultFromService(r *service.BulkAssignResult) *BulkAssignResult {
  876. if r == nil {
  877. return nil
  878. }
  879. subs := make([]AdminUserSubscription, 0, len(r.Subscriptions))
  880. for i := range r.Subscriptions {
  881. subs = append(subs, *UserSubscriptionFromServiceAdmin(&r.Subscriptions[i]))
  882. }
  883. statuses := make(map[string]string, len(r.Statuses))
  884. for userID, status := range r.Statuses {
  885. statuses[strconv.FormatInt(userID, 10)] = status
  886. }
  887. return &BulkAssignResult{
  888. SuccessCount: r.SuccessCount,
  889. CreatedCount: r.CreatedCount,
  890. ReusedCount: r.ReusedCount,
  891. FailedCount: r.FailedCount,
  892. Subscriptions: subs,
  893. Errors: r.Errors,
  894. Statuses: statuses,
  895. }
  896. }
  897. func PromoCodeFromService(pc *service.PromoCode) *PromoCode {
  898. if pc == nil {
  899. return nil
  900. }
  901. return &PromoCode{
  902. ID: pc.ID,
  903. Code: pc.Code,
  904. BonusAmount: pc.BonusAmount,
  905. MaxUses: pc.MaxUses,
  906. UsedCount: pc.UsedCount,
  907. Status: pc.Status,
  908. ExpiresAt: pc.ExpiresAt,
  909. Notes: pc.Notes,
  910. CreatedAt: pc.CreatedAt,
  911. UpdatedAt: pc.UpdatedAt,
  912. }
  913. }
  914. func PromoCodeUsageFromService(u *service.PromoCodeUsage) *PromoCodeUsage {
  915. if u == nil {
  916. return nil
  917. }
  918. return &PromoCodeUsage{
  919. ID: u.ID,
  920. PromoCodeID: u.PromoCodeID,
  921. UserID: u.UserID,
  922. BonusAmount: u.BonusAmount,
  923. UsedAt: u.UsedAt,
  924. User: UserFromServiceShallow(u.User),
  925. }
  926. }
  927. // SecurityPolicyKeywordFromService converts a service keyword to admin DTO.
  928. func SecurityPolicyKeywordFromService(w *service.SecurityPolicyKeyword) *SecurityPolicyKeyword {
  929. if w == nil {
  930. return nil
  931. }
  932. return &SecurityPolicyKeyword{
  933. ID: w.ID,
  934. GroupID: w.GroupID,
  935. Keyword: w.Keyword,
  936. Category: w.Category,
  937. Enabled: w.Enabled,
  938. CreatedAt: w.CreatedAt,
  939. UpdatedAt: w.UpdatedAt,
  940. }
  941. }
  942. // SecurityPolicyKeywordsFromService converts a service keyword list to admin DTOs.
  943. func SecurityPolicyKeywordsFromService(words []service.SecurityPolicyKeyword) []SecurityPolicyKeyword {
  944. out := make([]SecurityPolicyKeyword, 0, len(words))
  945. for i := range words {
  946. if dto := SecurityPolicyKeywordFromService(&words[i]); dto != nil {
  947. out = append(out, *dto)
  948. }
  949. }
  950. return out
  951. }
  952. // SecurityPolicyKeywordSeedFromService converts a builtin seed to admin DTO.
  953. func SecurityPolicyKeywordSeedFromService(seed service.SecurityPolicyKeywordSeed) SecurityPolicyKeywordSeed {
  954. return SecurityPolicyKeywordSeed{Keyword: seed.Keyword, Category: seed.Category}
  955. }
  956. // ProxyWithAccountCountFromServiceAdmin converts a service ProxyWithAccountCount to AdminProxyWithAccountCount DTO.
  957. // It includes the password field - user-facing endpoints must not use this.