usage_log_repo_insert.go 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "strconv"
  9. "strings"
  10. "sync/atomic"
  11. "time"
  12. dbent "github.com/Wei-Shaw/sub2api/ent"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  14. "github.com/Wei-Shaw/sub2api/internal/service"
  15. )
  16. // usageLogInsertArgTypes must stay in the same order as:
  17. // 1. prepareUsageLogInsert().args
  18. // 2. every INSERT/CTE VALUES column list in this file
  19. // 3. execUsageLogInsertNoResult placeholder positions
  20. // 4. scanUsageLog selected column order (via usageLogSelectColumns)
  21. //
  22. // When adding a usage_logs column, update all of those call sites together.
  23. var usageLogInsertArgTypes = [...]string{
  24. "bigint", // user_id
  25. "bigint", // api_key_id
  26. "bigint", // account_id
  27. "text", // request_id
  28. "text", // model
  29. "text", // requested_model
  30. "text", // upstream_model
  31. "text", // upstream_response_model
  32. "boolean", // upstream_model_mismatch
  33. "bigint", // group_id
  34. "bigint", // subscription_id
  35. "integer", // input_tokens
  36. "integer", // output_tokens
  37. "integer", // cache_creation_tokens
  38. "integer", // cache_read_tokens
  39. "integer", // cache_creation_5m_tokens
  40. "integer", // cache_creation_1h_tokens
  41. "integer", // image_output_tokens
  42. "numeric", // image_output_cost
  43. "integer", // image_input_tokens
  44. "numeric", // image_input_cost
  45. "numeric", // input_cost
  46. "numeric", // output_cost
  47. "numeric", // cache_creation_cost
  48. "numeric", // cache_read_cost
  49. "numeric", // total_cost
  50. "numeric", // actual_cost
  51. "numeric", // rate_multiplier
  52. "numeric", // account_rate_multiplier
  53. "smallint", // billing_type
  54. "smallint", // request_type
  55. "boolean", // stream
  56. "boolean", // openai_ws_mode
  57. "integer", // duration_ms
  58. "integer", // first_token_ms
  59. "text", // user_agent
  60. "text", // ip_address
  61. "integer", // image_count
  62. "text", // image_size
  63. "text", // image_input_size
  64. "text", // image_output_size
  65. "text", // image_size_source
  66. "jsonb", // image_size_breakdown
  67. "integer", // video_count
  68. "text", // video_resolution
  69. "integer", // video_duration_seconds
  70. "text", // service_tier
  71. "text", // reasoning_effort
  72. "text", // requested_reasoning_effort
  73. "text", // inbound_endpoint
  74. "text", // upstream_endpoint
  75. "boolean", // cache_ttl_overridden
  76. "boolean", // long_context_billing_applied
  77. "bigint", // channel_id
  78. "text", // model_mapping_chain
  79. "text", // billing_tier
  80. "text", // billing_mode
  81. "numeric", // account_stats_cost
  82. "text", // upstream_request_id
  83. "text", // session_id
  84. "boolean", // native_compaction_v2
  85. "timestamptz", // created_at
  86. }
  87. const (
  88. usageLogCreateBatchMaxSize = 64
  89. usageLogCreateBatchWindow = 3 * time.Millisecond
  90. usageLogCreateBatchQueueCap = 4096
  91. usageLogCreateCancelWait = 2 * time.Second
  92. usageLogBestEffortBatchMaxSize = 256
  93. usageLogBestEffortBatchWindow = 20 * time.Millisecond
  94. usageLogBestEffortBatchQueueCap = 32768
  95. usageLogBestEffortRecentTTL = 30 * time.Second
  96. )
  97. type usageLogCreateRequest struct {
  98. log *service.UsageLog
  99. prepared usageLogInsertPrepared
  100. shared *usageLogCreateShared
  101. resultCh chan usageLogCreateResult
  102. }
  103. type usageLogCreateResult struct {
  104. inserted bool
  105. err error
  106. }
  107. type usageLogBestEffortRequest struct {
  108. prepared usageLogInsertPrepared
  109. apiKeyID int64
  110. resultCh chan error
  111. }
  112. type usageLogInsertPrepared struct {
  113. createdAt time.Time
  114. requestID string
  115. rateMultiplier float64
  116. requestType int16
  117. args []any
  118. }
  119. type usageLogBatchState struct {
  120. ID int64
  121. CreatedAt time.Time
  122. }
  123. type usageLogBatchRow struct {
  124. RequestID string `json:"request_id"`
  125. APIKeyID int64 `json:"api_key_id"`
  126. ID int64 `json:"id"`
  127. CreatedAt time.Time `json:"created_at"`
  128. Inserted bool `json:"inserted"`
  129. }
  130. type usageLogCreateShared struct {
  131. state atomic.Int32
  132. }
  133. const (
  134. usageLogCreateStateQueued int32 = iota
  135. usageLogCreateStateProcessing
  136. usageLogCreateStateCompleted
  137. usageLogCreateStateCanceled
  138. )
  139. func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) (bool, error) {
  140. if log == nil {
  141. return false, nil
  142. }
  143. if tx := dbent.TxFromContext(ctx); tx != nil {
  144. return r.createSingle(ctx, tx.Client(), log)
  145. }
  146. requestID := strings.TrimSpace(log.RequestID)
  147. if requestID == "" {
  148. return r.createSingle(ctx, r.sql, log)
  149. }
  150. log.RequestID = requestID
  151. return r.createBatched(ctx, log)
  152. }
  153. func (r *usageLogRepository) CreateBestEffort(ctx context.Context, log *service.UsageLog) error {
  154. if log == nil {
  155. return nil
  156. }
  157. if tx := dbent.TxFromContext(ctx); tx != nil {
  158. _, err := r.createSingle(ctx, tx.Client(), log)
  159. return err
  160. }
  161. if r.db == nil {
  162. _, err := r.createSingle(ctx, r.sql, log)
  163. return err
  164. }
  165. r.ensureBestEffortBatcher()
  166. if r.bestEffortBatchCh == nil {
  167. _, err := r.createSingle(ctx, r.sql, log)
  168. return err
  169. }
  170. req := usageLogBestEffortRequest{
  171. prepared: prepareUsageLogInsert(log),
  172. apiKeyID: log.APIKeyID,
  173. resultCh: make(chan error, 1),
  174. }
  175. if key, ok := r.bestEffortRecentKey(req.prepared.requestID, req.apiKeyID); ok {
  176. if _, exists := r.bestEffortRecent.Get(key); exists {
  177. return nil
  178. }
  179. }
  180. // 队列满时阻塞等待而非立即丢弃:批处理器持续排空队列,短暂等待即可入队。
  181. // 立即丢弃会造成“已扣费但无 usage_log”的永久数据缺口(issue #3656);
  182. // 阻塞上限由调用方 ctx 期限约束,超时后由上层同步兜底。
  183. select {
  184. case r.bestEffortBatchCh <- req:
  185. case <-ctx.Done():
  186. return service.MarkUsageLogCreateDropped(ctx.Err())
  187. }
  188. select {
  189. case err := <-req.resultCh:
  190. return err
  191. case <-ctx.Done():
  192. return service.MarkUsageLogCreateDropped(ctx.Err())
  193. }
  194. }
  195. func (r *usageLogRepository) createSingle(ctx context.Context, sqlq sqlExecutor, log *service.UsageLog) (bool, error) {
  196. prepared := prepareUsageLogInsert(log)
  197. if sqlq == nil {
  198. sqlq = r.sql
  199. }
  200. if ctx != nil && ctx.Err() != nil {
  201. return false, service.MarkUsageLogCreateNotPersisted(ctx.Err())
  202. }
  203. query := `
  204. INSERT INTO usage_logs (
  205. user_id,
  206. api_key_id,
  207. account_id,
  208. request_id,
  209. model,
  210. requested_model,
  211. upstream_model,
  212. upstream_response_model,
  213. upstream_model_mismatch,
  214. group_id,
  215. subscription_id,
  216. input_tokens,
  217. output_tokens,
  218. cache_creation_tokens,
  219. cache_read_tokens,
  220. cache_creation_5m_tokens,
  221. cache_creation_1h_tokens,
  222. image_output_tokens,
  223. image_output_cost,
  224. image_input_tokens,
  225. image_input_cost,
  226. input_cost,
  227. output_cost,
  228. cache_creation_cost,
  229. cache_read_cost,
  230. total_cost,
  231. actual_cost,
  232. rate_multiplier,
  233. account_rate_multiplier,
  234. billing_type,
  235. request_type,
  236. stream,
  237. openai_ws_mode,
  238. duration_ms,
  239. first_token_ms,
  240. user_agent,
  241. ip_address,
  242. image_count,
  243. image_size,
  244. image_input_size,
  245. image_output_size,
  246. image_size_source,
  247. image_size_breakdown,
  248. video_count,
  249. video_resolution,
  250. video_duration_seconds,
  251. service_tier,
  252. reasoning_effort,
  253. requested_reasoning_effort,
  254. inbound_endpoint,
  255. upstream_endpoint,
  256. cache_ttl_overridden,
  257. long_context_billing_applied,
  258. channel_id,
  259. model_mapping_chain,
  260. billing_tier,
  261. billing_mode,
  262. account_stats_cost,
  263. upstream_request_id,
  264. session_id,
  265. native_compaction_v2,
  266. created_at
  267. ) VALUES (
  268. $1, $2, $3, $4, $5, $6, $7, $8, $9,
  269. $10, $11,
  270. $12, $13, $14, $15,
  271. $16, $17, $18, $19,
  272. $20, $21, $22, $23, $24, $25,
  273. $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62
  274. )
  275. ON CONFLICT (request_id, api_key_id) DO NOTHING
  276. RETURNING id, created_at
  277. `
  278. if err := scanSingleRow(ctx, sqlq, query, prepared.args, &log.ID, &log.CreatedAt); err != nil {
  279. if errors.Is(err, sql.ErrNoRows) && prepared.requestID != "" {
  280. selectQuery := "SELECT id, created_at FROM usage_logs WHERE request_id = $1 AND api_key_id = $2"
  281. if err := scanSingleRow(ctx, sqlq, selectQuery, []any{prepared.requestID, log.APIKeyID}, &log.ID, &log.CreatedAt); err != nil {
  282. return false, err
  283. }
  284. log.RateMultiplier = prepared.rateMultiplier
  285. return false, nil
  286. } else {
  287. return false, err
  288. }
  289. }
  290. log.RateMultiplier = prepared.rateMultiplier
  291. return true, nil
  292. }
  293. func (r *usageLogRepository) createBatched(ctx context.Context, log *service.UsageLog) (bool, error) {
  294. if r.db == nil {
  295. return r.createSingle(ctx, r.sql, log)
  296. }
  297. r.ensureCreateBatcher()
  298. if r.createBatchCh == nil {
  299. return r.createSingle(ctx, r.sql, log)
  300. }
  301. req := usageLogCreateRequest{
  302. log: log,
  303. prepared: prepareUsageLogInsert(log),
  304. shared: &usageLogCreateShared{},
  305. resultCh: make(chan usageLogCreateResult, 1),
  306. }
  307. // 队列满时阻塞等待而非立即报错:本路径是 best-effort 丢弃后的最后兜底,
  308. // 立即失败会让日志永久丢失;阻塞上限由调用方 ctx 期限约束。
  309. select {
  310. case r.createBatchCh <- req:
  311. case <-ctx.Done():
  312. return false, service.MarkUsageLogCreateNotPersisted(ctx.Err())
  313. }
  314. select {
  315. case res := <-req.resultCh:
  316. return res.inserted, res.err
  317. case <-ctx.Done():
  318. if req.shared != nil && req.shared.state.CompareAndSwap(usageLogCreateStateQueued, usageLogCreateStateCanceled) {
  319. return false, service.MarkUsageLogCreateNotPersisted(ctx.Err())
  320. }
  321. timer := time.NewTimer(usageLogCreateCancelWait)
  322. defer timer.Stop()
  323. select {
  324. case res := <-req.resultCh:
  325. return res.inserted, res.err
  326. case <-timer.C:
  327. return false, ctx.Err()
  328. }
  329. }
  330. }
  331. func (r *usageLogRepository) ensureCreateBatcher() {
  332. if r == nil || r.db == nil {
  333. return
  334. }
  335. // nil 检查必须在 Once 内部:在外层做无同步快路径读会与 Once 内的写构成数据竞争。
  336. r.createBatchOnce.Do(func() {
  337. if r.createBatchCh == nil {
  338. r.createBatchCh = make(chan usageLogCreateRequest, usageLogCreateBatchQueueCap)
  339. go r.runCreateBatcher(r.db)
  340. }
  341. })
  342. }
  343. func (r *usageLogRepository) ensureBestEffortBatcher() {
  344. if r == nil || r.db == nil {
  345. return
  346. }
  347. // 同 ensureCreateBatcher:nil 检查放在 Once 内部以避免数据竞争。
  348. r.bestEffortBatchOnce.Do(func() {
  349. if r.bestEffortBatchCh == nil {
  350. r.bestEffortBatchCh = make(chan usageLogBestEffortRequest, usageLogBestEffortBatchQueueCap)
  351. go r.runBestEffortBatcher(r.db)
  352. }
  353. })
  354. }
  355. func (r *usageLogRepository) runCreateBatcher(db *sql.DB) {
  356. for {
  357. first, ok := <-r.createBatchCh
  358. if !ok {
  359. return
  360. }
  361. batch := make([]usageLogCreateRequest, 0, usageLogCreateBatchMaxSize)
  362. batch = append(batch, first)
  363. timer := time.NewTimer(usageLogCreateBatchWindow)
  364. batchLoop:
  365. for len(batch) < usageLogCreateBatchMaxSize {
  366. select {
  367. case req, ok := <-r.createBatchCh:
  368. if !ok {
  369. break batchLoop
  370. }
  371. batch = append(batch, req)
  372. case <-timer.C:
  373. break batchLoop
  374. }
  375. }
  376. if !timer.Stop() {
  377. select {
  378. case <-timer.C:
  379. default:
  380. }
  381. }
  382. r.flushCreateBatch(db, batch)
  383. }
  384. }
  385. func (r *usageLogRepository) runBestEffortBatcher(db *sql.DB) {
  386. for {
  387. first, ok := <-r.bestEffortBatchCh
  388. if !ok {
  389. return
  390. }
  391. batch := make([]usageLogBestEffortRequest, 0, usageLogBestEffortBatchMaxSize)
  392. batch = append(batch, first)
  393. timer := time.NewTimer(usageLogBestEffortBatchWindow)
  394. bestEffortLoop:
  395. for len(batch) < usageLogBestEffortBatchMaxSize {
  396. select {
  397. case req, ok := <-r.bestEffortBatchCh:
  398. if !ok {
  399. break bestEffortLoop
  400. }
  401. batch = append(batch, req)
  402. case <-timer.C:
  403. break bestEffortLoop
  404. }
  405. }
  406. if !timer.Stop() {
  407. select {
  408. case <-timer.C:
  409. default:
  410. }
  411. }
  412. r.flushBestEffortBatch(db, batch)
  413. }
  414. }
  415. func (r *usageLogRepository) flushCreateBatch(db *sql.DB, batch []usageLogCreateRequest) {
  416. if len(batch) == 0 {
  417. return
  418. }
  419. uniqueOrder := make([]string, 0, len(batch))
  420. preparedByKey := make(map[string]usageLogInsertPrepared, len(batch))
  421. requestsByKey := make(map[string][]usageLogCreateRequest, len(batch))
  422. fallback := make([]usageLogCreateRequest, 0)
  423. for _, req := range batch {
  424. if req.log == nil {
  425. completeUsageLogCreateRequest(req, usageLogCreateResult{inserted: false, err: nil})
  426. continue
  427. }
  428. if req.shared != nil && !req.shared.state.CompareAndSwap(usageLogCreateStateQueued, usageLogCreateStateProcessing) {
  429. if req.shared.state.Load() == usageLogCreateStateCanceled {
  430. completeUsageLogCreateRequest(req, usageLogCreateResult{
  431. inserted: false,
  432. err: service.MarkUsageLogCreateNotPersisted(context.Canceled),
  433. })
  434. continue
  435. }
  436. }
  437. prepared := req.prepared
  438. if prepared.requestID == "" {
  439. fallback = append(fallback, req)
  440. continue
  441. }
  442. key := usageLogBatchKey(prepared.requestID, req.log.APIKeyID)
  443. if _, exists := requestsByKey[key]; !exists {
  444. uniqueOrder = append(uniqueOrder, key)
  445. preparedByKey[key] = prepared
  446. }
  447. requestsByKey[key] = append(requestsByKey[key], req)
  448. }
  449. if len(uniqueOrder) > 0 {
  450. insertedMap, stateMap, safeFallback, err := r.batchInsertUsageLogs(db, uniqueOrder, preparedByKey)
  451. if err != nil {
  452. if safeFallback {
  453. for _, key := range uniqueOrder {
  454. fallback = append(fallback, requestsByKey[key]...)
  455. }
  456. } else {
  457. for _, key := range uniqueOrder {
  458. reqs := requestsByKey[key]
  459. state, hasState := stateMap[key]
  460. inserted := insertedMap[key]
  461. for idx, req := range reqs {
  462. req.log.RateMultiplier = preparedByKey[key].rateMultiplier
  463. if hasState {
  464. req.log.ID = state.ID
  465. req.log.CreatedAt = state.CreatedAt
  466. }
  467. switch {
  468. case inserted && idx == 0:
  469. completeUsageLogCreateRequest(req, usageLogCreateResult{inserted: true, err: nil})
  470. case inserted:
  471. completeUsageLogCreateRequest(req, usageLogCreateResult{inserted: false, err: nil})
  472. case hasState:
  473. completeUsageLogCreateRequest(req, usageLogCreateResult{inserted: false, err: nil})
  474. case idx == 0:
  475. completeUsageLogCreateRequest(req, usageLogCreateResult{inserted: false, err: err})
  476. default:
  477. completeUsageLogCreateRequest(req, usageLogCreateResult{inserted: false, err: nil})
  478. }
  479. }
  480. }
  481. }
  482. } else {
  483. for _, key := range uniqueOrder {
  484. reqs := requestsByKey[key]
  485. state, ok := stateMap[key]
  486. if !ok {
  487. for _, req := range reqs {
  488. completeUsageLogCreateRequest(req, usageLogCreateResult{
  489. inserted: false,
  490. err: fmt.Errorf("usage log batch state missing for key=%s", key),
  491. })
  492. }
  493. continue
  494. }
  495. for idx, req := range reqs {
  496. req.log.ID = state.ID
  497. req.log.CreatedAt = state.CreatedAt
  498. req.log.RateMultiplier = preparedByKey[key].rateMultiplier
  499. completeUsageLogCreateRequest(req, usageLogCreateResult{
  500. inserted: idx == 0 && insertedMap[key],
  501. err: nil,
  502. })
  503. }
  504. }
  505. }
  506. }
  507. if len(fallback) == 0 {
  508. return
  509. }
  510. for _, req := range fallback {
  511. fallbackCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  512. inserted, err := r.createSingle(fallbackCtx, db, req.log)
  513. cancel()
  514. completeUsageLogCreateRequest(req, usageLogCreateResult{inserted: inserted, err: err})
  515. }
  516. }
  517. func (r *usageLogRepository) flushBestEffortBatch(db *sql.DB, batch []usageLogBestEffortRequest) {
  518. if len(batch) == 0 {
  519. return
  520. }
  521. type bestEffortGroup struct {
  522. prepared usageLogInsertPrepared
  523. apiKeyID int64
  524. key string
  525. reqs []usageLogBestEffortRequest
  526. }
  527. groupsByKey := make(map[string]*bestEffortGroup, len(batch))
  528. groupOrder := make([]*bestEffortGroup, 0, len(batch))
  529. preparedList := make([]usageLogInsertPrepared, 0, len(batch))
  530. for idx, req := range batch {
  531. prepared := req.prepared
  532. key := fmt.Sprintf("__best_effort_%d", idx)
  533. if prepared.requestID != "" {
  534. key = usageLogBatchKey(prepared.requestID, req.apiKeyID)
  535. }
  536. group, exists := groupsByKey[key]
  537. if !exists {
  538. group = &bestEffortGroup{
  539. prepared: prepared,
  540. apiKeyID: req.apiKeyID,
  541. key: key,
  542. }
  543. groupsByKey[key] = group
  544. groupOrder = append(groupOrder, group)
  545. preparedList = append(preparedList, prepared)
  546. }
  547. group.reqs = append(group.reqs, req)
  548. }
  549. if len(preparedList) == 0 {
  550. for _, req := range batch {
  551. sendUsageLogBestEffortResult(req.resultCh, nil)
  552. }
  553. return
  554. }
  555. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  556. defer cancel()
  557. query, args := buildUsageLogBestEffortInsertQuery(preparedList)
  558. if _, err := db.ExecContext(ctx, query, args...); err != nil {
  559. logger.LegacyPrintf("repository.usage_log", "best-effort batch insert failed: %v", err)
  560. for _, group := range groupOrder {
  561. singleErr := execUsageLogInsertNoResult(ctx, db, group.prepared)
  562. if singleErr != nil {
  563. logger.LegacyPrintf("repository.usage_log", "best-effort single fallback insert failed: %v", singleErr)
  564. } else if group.prepared.requestID != "" && r != nil && r.bestEffortRecent != nil {
  565. r.bestEffortRecent.SetDefault(group.key, struct{}{})
  566. }
  567. for _, req := range group.reqs {
  568. sendUsageLogBestEffortResult(req.resultCh, singleErr)
  569. }
  570. }
  571. return
  572. }
  573. for _, group := range groupOrder {
  574. if group.prepared.requestID != "" && r != nil && r.bestEffortRecent != nil {
  575. r.bestEffortRecent.SetDefault(group.key, struct{}{})
  576. }
  577. for _, req := range group.reqs {
  578. sendUsageLogBestEffortResult(req.resultCh, nil)
  579. }
  580. }
  581. }
  582. func sendUsageLogBestEffortResult(ch chan error, err error) {
  583. if ch == nil {
  584. return
  585. }
  586. select {
  587. case ch <- err:
  588. default:
  589. }
  590. }
  591. func completeUsageLogCreateRequest(req usageLogCreateRequest, res usageLogCreateResult) {
  592. if req.shared != nil {
  593. req.shared.state.Store(usageLogCreateStateCompleted)
  594. }
  595. sendUsageLogCreateResult(req.resultCh, res)
  596. }
  597. func (r *usageLogRepository) batchInsertUsageLogs(db *sql.DB, keys []string, preparedByKey map[string]usageLogInsertPrepared) (map[string]bool, map[string]usageLogBatchState, bool, error) {
  598. if len(keys) == 0 {
  599. return map[string]bool{}, map[string]usageLogBatchState{}, false, nil
  600. }
  601. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  602. defer cancel()
  603. query, args := buildUsageLogBatchInsertQuery(keys, preparedByKey)
  604. var payload []byte
  605. if err := db.QueryRowContext(ctx, query, args...).Scan(&payload); err != nil {
  606. return nil, nil, true, err
  607. }
  608. var rows []usageLogBatchRow
  609. if err := json.Unmarshal(payload, &rows); err != nil {
  610. return nil, nil, false, err
  611. }
  612. insertedMap := make(map[string]bool, len(keys))
  613. stateMap := make(map[string]usageLogBatchState, len(keys))
  614. for _, row := range rows {
  615. key := usageLogBatchKey(row.RequestID, row.APIKeyID)
  616. insertedMap[key] = row.Inserted
  617. stateMap[key] = usageLogBatchState{
  618. ID: row.ID,
  619. CreatedAt: row.CreatedAt,
  620. }
  621. }
  622. if len(stateMap) != len(keys) {
  623. return insertedMap, stateMap, false, fmt.Errorf("usage log batch state count mismatch: got=%d want=%d", len(stateMap), len(keys))
  624. }
  625. return insertedMap, stateMap, false, nil
  626. }
  627. func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usageLogInsertPrepared) (string, []any) {
  628. var query strings.Builder
  629. _, _ = query.WriteString(`
  630. WITH input (
  631. input_idx,
  632. user_id,
  633. api_key_id,
  634. account_id,
  635. request_id,
  636. model,
  637. requested_model,
  638. upstream_model,
  639. upstream_response_model,
  640. upstream_model_mismatch,
  641. group_id,
  642. subscription_id,
  643. input_tokens,
  644. output_tokens,
  645. cache_creation_tokens,
  646. cache_read_tokens,
  647. cache_creation_5m_tokens,
  648. cache_creation_1h_tokens,
  649. image_output_tokens,
  650. image_output_cost,
  651. image_input_tokens,
  652. image_input_cost,
  653. input_cost,
  654. output_cost,
  655. cache_creation_cost,
  656. cache_read_cost,
  657. total_cost,
  658. actual_cost,
  659. rate_multiplier,
  660. account_rate_multiplier,
  661. billing_type,
  662. request_type,
  663. stream,
  664. openai_ws_mode,
  665. duration_ms,
  666. first_token_ms,
  667. user_agent,
  668. ip_address,
  669. image_count,
  670. image_size,
  671. image_input_size,
  672. image_output_size,
  673. image_size_source,
  674. image_size_breakdown,
  675. video_count,
  676. video_resolution,
  677. video_duration_seconds,
  678. service_tier,
  679. reasoning_effort,
  680. requested_reasoning_effort,
  681. inbound_endpoint,
  682. upstream_endpoint,
  683. cache_ttl_overridden,
  684. long_context_billing_applied,
  685. channel_id,
  686. model_mapping_chain,
  687. billing_tier,
  688. billing_mode,
  689. account_stats_cost,
  690. upstream_request_id,
  691. session_id,
  692. native_compaction_v2,
  693. created_at
  694. ) AS (VALUES `)
  695. // Each batch row prepends the synthetic input_index before the 60
  696. // usage-log column values.
  697. args := make([]any, 0, len(keys)*61)
  698. argPos := 1
  699. for idx, key := range keys {
  700. if idx > 0 {
  701. _, _ = query.WriteString(",")
  702. }
  703. _, _ = query.WriteString("(")
  704. _, _ = query.WriteString("$")
  705. _, _ = query.WriteString(strconv.Itoa(argPos))
  706. args = append(args, idx)
  707. argPos++
  708. prepared := preparedByKey[key]
  709. for i := 0; i < len(prepared.args); i++ {
  710. _, _ = query.WriteString(",")
  711. _, _ = query.WriteString("$")
  712. _, _ = query.WriteString(strconv.Itoa(argPos))
  713. if i < len(usageLogInsertArgTypes) {
  714. _, _ = query.WriteString("::")
  715. _, _ = query.WriteString(usageLogInsertArgTypes[i])
  716. }
  717. argPos++
  718. }
  719. _, _ = query.WriteString(")")
  720. args = append(args, prepared.args...)
  721. }
  722. _, _ = query.WriteString(`
  723. ),
  724. inserted AS (
  725. INSERT INTO usage_logs (
  726. user_id,
  727. api_key_id,
  728. account_id,
  729. request_id,
  730. model,
  731. requested_model,
  732. upstream_model,
  733. upstream_response_model,
  734. upstream_model_mismatch,
  735. group_id,
  736. subscription_id,
  737. input_tokens,
  738. output_tokens,
  739. cache_creation_tokens,
  740. cache_read_tokens,
  741. cache_creation_5m_tokens,
  742. cache_creation_1h_tokens,
  743. image_output_tokens,
  744. image_output_cost,
  745. image_input_tokens,
  746. image_input_cost,
  747. input_cost,
  748. output_cost,
  749. cache_creation_cost,
  750. cache_read_cost,
  751. total_cost,
  752. actual_cost,
  753. rate_multiplier,
  754. account_rate_multiplier,
  755. billing_type,
  756. request_type,
  757. stream,
  758. openai_ws_mode,
  759. duration_ms,
  760. first_token_ms,
  761. user_agent,
  762. ip_address,
  763. image_count,
  764. image_size,
  765. image_input_size,
  766. image_output_size,
  767. image_size_source,
  768. image_size_breakdown,
  769. video_count,
  770. video_resolution,
  771. video_duration_seconds,
  772. service_tier,
  773. reasoning_effort,
  774. requested_reasoning_effort,
  775. inbound_endpoint,
  776. upstream_endpoint,
  777. cache_ttl_overridden,
  778. long_context_billing_applied,
  779. channel_id,
  780. model_mapping_chain,
  781. billing_tier,
  782. billing_mode,
  783. account_stats_cost,
  784. upstream_request_id,
  785. session_id,
  786. native_compaction_v2,
  787. created_at
  788. )
  789. SELECT
  790. user_id,
  791. api_key_id,
  792. account_id,
  793. request_id,
  794. model,
  795. requested_model,
  796. upstream_model,
  797. upstream_response_model,
  798. upstream_model_mismatch,
  799. group_id,
  800. subscription_id,
  801. input_tokens,
  802. output_tokens,
  803. cache_creation_tokens,
  804. cache_read_tokens,
  805. cache_creation_5m_tokens,
  806. cache_creation_1h_tokens,
  807. image_output_tokens,
  808. image_output_cost,
  809. image_input_tokens,
  810. image_input_cost,
  811. input_cost,
  812. output_cost,
  813. cache_creation_cost,
  814. cache_read_cost,
  815. total_cost,
  816. actual_cost,
  817. rate_multiplier,
  818. account_rate_multiplier,
  819. billing_type,
  820. request_type,
  821. stream,
  822. openai_ws_mode,
  823. duration_ms,
  824. first_token_ms,
  825. user_agent,
  826. ip_address,
  827. image_count,
  828. image_size,
  829. image_input_size,
  830. image_output_size,
  831. image_size_source,
  832. image_size_breakdown,
  833. video_count,
  834. video_resolution,
  835. video_duration_seconds,
  836. service_tier,
  837. reasoning_effort,
  838. requested_reasoning_effort,
  839. inbound_endpoint,
  840. upstream_endpoint,
  841. cache_ttl_overridden,
  842. long_context_billing_applied,
  843. channel_id,
  844. model_mapping_chain,
  845. billing_tier,
  846. billing_mode,
  847. account_stats_cost,
  848. upstream_request_id,
  849. session_id,
  850. native_compaction_v2,
  851. created_at
  852. FROM input
  853. ON CONFLICT (request_id, api_key_id) DO NOTHING
  854. RETURNING request_id, api_key_id, id, created_at
  855. ),
  856. resolved AS (
  857. SELECT
  858. input.input_idx,
  859. input.request_id,
  860. input.api_key_id,
  861. COALESCE(inserted.id, existing.id) AS id,
  862. COALESCE(inserted.created_at, existing.created_at) AS created_at,
  863. (inserted.id IS NOT NULL) AS inserted
  864. FROM input
  865. LEFT JOIN inserted
  866. ON inserted.request_id = input.request_id
  867. AND inserted.api_key_id = input.api_key_id
  868. LEFT JOIN usage_logs existing
  869. ON existing.request_id = input.request_id
  870. AND existing.api_key_id = input.api_key_id
  871. )
  872. SELECT COALESCE(
  873. json_agg(
  874. json_build_object(
  875. 'request_id', resolved.request_id,
  876. 'api_key_id', resolved.api_key_id,
  877. 'id', resolved.id,
  878. 'created_at', resolved.created_at,
  879. 'inserted', resolved.inserted
  880. )
  881. ORDER BY resolved.input_idx
  882. ),
  883. '[]'::json
  884. )
  885. FROM resolved
  886. `)
  887. return query.String(), args
  888. }
  889. func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (string, []any) {
  890. var query strings.Builder
  891. _, _ = query.WriteString(`
  892. WITH input (
  893. user_id,
  894. api_key_id,
  895. account_id,
  896. request_id,
  897. model,
  898. requested_model,
  899. upstream_model,
  900. upstream_response_model,
  901. upstream_model_mismatch,
  902. group_id,
  903. subscription_id,
  904. input_tokens,
  905. output_tokens,
  906. cache_creation_tokens,
  907. cache_read_tokens,
  908. cache_creation_5m_tokens,
  909. cache_creation_1h_tokens,
  910. image_output_tokens,
  911. image_output_cost,
  912. image_input_tokens,
  913. image_input_cost,
  914. input_cost,
  915. output_cost,
  916. cache_creation_cost,
  917. cache_read_cost,
  918. total_cost,
  919. actual_cost,
  920. rate_multiplier,
  921. account_rate_multiplier,
  922. billing_type,
  923. request_type,
  924. stream,
  925. openai_ws_mode,
  926. duration_ms,
  927. first_token_ms,
  928. user_agent,
  929. ip_address,
  930. image_count,
  931. image_size,
  932. image_input_size,
  933. image_output_size,
  934. image_size_source,
  935. image_size_breakdown,
  936. video_count,
  937. video_resolution,
  938. video_duration_seconds,
  939. service_tier,
  940. reasoning_effort,
  941. requested_reasoning_effort,
  942. inbound_endpoint,
  943. upstream_endpoint,
  944. cache_ttl_overridden,
  945. long_context_billing_applied,
  946. channel_id,
  947. model_mapping_chain,
  948. billing_tier,
  949. billing_mode,
  950. account_stats_cost,
  951. upstream_request_id,
  952. session_id,
  953. native_compaction_v2,
  954. created_at
  955. ) AS (VALUES `)
  956. args := make([]any, 0, len(preparedList)*60)
  957. argPos := 1
  958. for idx, prepared := range preparedList {
  959. if idx > 0 {
  960. _, _ = query.WriteString(",")
  961. }
  962. _, _ = query.WriteString("(")
  963. for i := 0; i < len(prepared.args); i++ {
  964. if i > 0 {
  965. _, _ = query.WriteString(",")
  966. }
  967. _, _ = query.WriteString("$")
  968. _, _ = query.WriteString(strconv.Itoa(argPos))
  969. if i < len(usageLogInsertArgTypes) {
  970. _, _ = query.WriteString("::")
  971. _, _ = query.WriteString(usageLogInsertArgTypes[i])
  972. }
  973. argPos++
  974. }
  975. _, _ = query.WriteString(")")
  976. args = append(args, prepared.args...)
  977. }
  978. _, _ = query.WriteString(`
  979. )
  980. INSERT INTO usage_logs (
  981. user_id,
  982. api_key_id,
  983. account_id,
  984. request_id,
  985. model,
  986. requested_model,
  987. upstream_model,
  988. upstream_response_model,
  989. upstream_model_mismatch,
  990. group_id,
  991. subscription_id,
  992. input_tokens,
  993. output_tokens,
  994. cache_creation_tokens,
  995. cache_read_tokens,
  996. cache_creation_5m_tokens,
  997. cache_creation_1h_tokens,
  998. image_output_tokens,
  999. image_output_cost,
  1000. image_input_tokens,
  1001. image_input_cost,
  1002. input_cost,
  1003. output_cost,
  1004. cache_creation_cost,
  1005. cache_read_cost,
  1006. total_cost,
  1007. actual_cost,
  1008. rate_multiplier,
  1009. account_rate_multiplier,
  1010. billing_type,
  1011. request_type,
  1012. stream,
  1013. openai_ws_mode,
  1014. duration_ms,
  1015. first_token_ms,
  1016. user_agent,
  1017. ip_address,
  1018. image_count,
  1019. image_size,
  1020. image_input_size,
  1021. image_output_size,
  1022. image_size_source,
  1023. image_size_breakdown,
  1024. video_count,
  1025. video_resolution,
  1026. video_duration_seconds,
  1027. service_tier,
  1028. reasoning_effort,
  1029. requested_reasoning_effort,
  1030. inbound_endpoint,
  1031. upstream_endpoint,
  1032. cache_ttl_overridden,
  1033. long_context_billing_applied,
  1034. channel_id,
  1035. model_mapping_chain,
  1036. billing_tier,
  1037. billing_mode,
  1038. account_stats_cost,
  1039. upstream_request_id,
  1040. session_id,
  1041. native_compaction_v2,
  1042. created_at
  1043. )
  1044. SELECT
  1045. user_id,
  1046. api_key_id,
  1047. account_id,
  1048. request_id,
  1049. model,
  1050. requested_model,
  1051. upstream_model,
  1052. upstream_response_model,
  1053. upstream_model_mismatch,
  1054. group_id,
  1055. subscription_id,
  1056. input_tokens,
  1057. output_tokens,
  1058. cache_creation_tokens,
  1059. cache_read_tokens,
  1060. cache_creation_5m_tokens,
  1061. cache_creation_1h_tokens,
  1062. image_output_tokens,
  1063. image_output_cost,
  1064. image_input_tokens,
  1065. image_input_cost,
  1066. input_cost,
  1067. output_cost,
  1068. cache_creation_cost,
  1069. cache_read_cost,
  1070. total_cost,
  1071. actual_cost,
  1072. rate_multiplier,
  1073. account_rate_multiplier,
  1074. billing_type,
  1075. request_type,
  1076. stream,
  1077. openai_ws_mode,
  1078. duration_ms,
  1079. first_token_ms,
  1080. user_agent,
  1081. ip_address,
  1082. image_count,
  1083. image_size,
  1084. image_input_size,
  1085. image_output_size,
  1086. image_size_source,
  1087. image_size_breakdown,
  1088. video_count,
  1089. video_resolution,
  1090. video_duration_seconds,
  1091. service_tier,
  1092. reasoning_effort,
  1093. requested_reasoning_effort,
  1094. inbound_endpoint,
  1095. upstream_endpoint,
  1096. cache_ttl_overridden,
  1097. long_context_billing_applied,
  1098. channel_id,
  1099. model_mapping_chain,
  1100. billing_tier,
  1101. billing_mode,
  1102. account_stats_cost,
  1103. upstream_request_id,
  1104. session_id,
  1105. native_compaction_v2,
  1106. created_at
  1107. FROM input
  1108. ON CONFLICT (request_id, api_key_id) DO NOTHING
  1109. `)
  1110. return query.String(), args
  1111. }
  1112. func execUsageLogInsertNoResult(ctx context.Context, sqlq sqlExecutor, prepared usageLogInsertPrepared) error {
  1113. _, err := sqlq.ExecContext(ctx, `
  1114. INSERT INTO usage_logs (
  1115. user_id,
  1116. api_key_id,
  1117. account_id,
  1118. request_id,
  1119. model,
  1120. requested_model,
  1121. upstream_model,
  1122. upstream_response_model,
  1123. upstream_model_mismatch,
  1124. group_id,
  1125. subscription_id,
  1126. input_tokens,
  1127. output_tokens,
  1128. cache_creation_tokens,
  1129. cache_read_tokens,
  1130. cache_creation_5m_tokens,
  1131. cache_creation_1h_tokens,
  1132. image_output_tokens,
  1133. image_output_cost,
  1134. image_input_tokens,
  1135. image_input_cost,
  1136. input_cost,
  1137. output_cost,
  1138. cache_creation_cost,
  1139. cache_read_cost,
  1140. total_cost,
  1141. actual_cost,
  1142. rate_multiplier,
  1143. account_rate_multiplier,
  1144. billing_type,
  1145. request_type,
  1146. stream,
  1147. openai_ws_mode,
  1148. duration_ms,
  1149. first_token_ms,
  1150. user_agent,
  1151. ip_address,
  1152. image_count,
  1153. image_size,
  1154. image_input_size,
  1155. image_output_size,
  1156. image_size_source,
  1157. image_size_breakdown,
  1158. video_count,
  1159. video_resolution,
  1160. video_duration_seconds,
  1161. service_tier,
  1162. reasoning_effort,
  1163. requested_reasoning_effort,
  1164. inbound_endpoint,
  1165. upstream_endpoint,
  1166. cache_ttl_overridden,
  1167. long_context_billing_applied,
  1168. channel_id,
  1169. model_mapping_chain,
  1170. billing_tier,
  1171. billing_mode,
  1172. account_stats_cost,
  1173. upstream_request_id,
  1174. session_id,
  1175. native_compaction_v2,
  1176. created_at
  1177. ) VALUES (
  1178. $1, $2, $3, $4, $5, $6, $7, $8, $9,
  1179. $10, $11,
  1180. $12, $13, $14, $15,
  1181. $16, $17, $18, $19,
  1182. $20, $21, $22, $23, $24, $25,
  1183. $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62
  1184. )
  1185. ON CONFLICT (request_id, api_key_id) DO NOTHING
  1186. `, prepared.args...)
  1187. return err
  1188. }
  1189. func prepareUsageLogInsert(log *service.UsageLog) usageLogInsertPrepared {
  1190. createdAt := log.CreatedAt
  1191. if createdAt.IsZero() {
  1192. createdAt = time.Now()
  1193. }
  1194. requestID := strings.TrimSpace(log.RequestID)
  1195. log.RequestID = requestID
  1196. rateMultiplier := log.RateMultiplier
  1197. log.SyncRequestTypeAndLegacyFields()
  1198. requestType := int16(log.RequestType)
  1199. groupID := nullInt64(log.GroupID)
  1200. subscriptionID := nullInt64(log.SubscriptionID)
  1201. duration := nullInt(log.DurationMs)
  1202. firstToken := nullInt(log.FirstTokenMs)
  1203. userAgent := nullString(log.UserAgent)
  1204. ipAddress := nullString(log.IPAddress)
  1205. imageSize := nullString(log.ImageSize)
  1206. imageInputSize := nullString(log.ImageInputSize)
  1207. imageOutputSize := nullString(log.ImageOutputSize)
  1208. imageSizeSource := nullString(log.ImageSizeSource)
  1209. imageSizeBreakdown := nullStringIntMapJSON(log.ImageSizeBreakdown)
  1210. videoResolution := nullString(log.VideoResolution)
  1211. videoDurationSeconds := nullInt(log.VideoDurationSeconds)
  1212. serviceTier := nullString(log.ServiceTier)
  1213. reasoningEffort := nullString(log.ReasoningEffort)
  1214. requestedReasoningEffort := nullString(log.RequestedReasoningEffort)
  1215. inboundEndpoint := nullString(log.InboundEndpoint)
  1216. upstreamEndpoint := nullString(log.UpstreamEndpoint)
  1217. channelID := nullInt64(log.ChannelID)
  1218. modelMappingChain := nullString(log.ModelMappingChain)
  1219. billingTier := nullString(log.BillingTier)
  1220. billingMode := nullString(log.BillingMode)
  1221. upstreamRequestID := nullString(log.UpstreamRequestID)
  1222. sessionID := nullString(log.SessionID)
  1223. requestedModel := strings.TrimSpace(log.RequestedModel)
  1224. if requestedModel == "" {
  1225. requestedModel = strings.TrimSpace(log.Model)
  1226. }
  1227. upstreamModel := nullString(log.UpstreamModel)
  1228. upstreamResponseModel := nullString(log.UpstreamResponseModel)
  1229. upstreamModelMismatch := nullBool(log.UpstreamModelMismatch)
  1230. var requestIDArg any
  1231. if requestID != "" {
  1232. requestIDArg = requestID
  1233. }
  1234. return usageLogInsertPrepared{
  1235. createdAt: createdAt,
  1236. requestID: requestID,
  1237. rateMultiplier: rateMultiplier,
  1238. requestType: requestType,
  1239. args: []any{
  1240. log.UserID,
  1241. log.APIKeyID,
  1242. log.AccountID,
  1243. requestIDArg,
  1244. log.Model,
  1245. nullString(&requestedModel),
  1246. upstreamModel,
  1247. upstreamResponseModel,
  1248. upstreamModelMismatch,
  1249. groupID,
  1250. subscriptionID,
  1251. log.InputTokens,
  1252. log.OutputTokens,
  1253. log.CacheCreationTokens,
  1254. log.CacheReadTokens,
  1255. log.CacheCreation5mTokens,
  1256. log.CacheCreation1hTokens,
  1257. log.ImageOutputTokens,
  1258. log.ImageOutputCost,
  1259. log.ImageInputTokens,
  1260. log.ImageInputCost,
  1261. log.InputCost,
  1262. log.OutputCost,
  1263. log.CacheCreationCost,
  1264. log.CacheReadCost,
  1265. log.TotalCost,
  1266. log.ActualCost,
  1267. rateMultiplier,
  1268. log.AccountRateMultiplier,
  1269. log.BillingType,
  1270. requestType,
  1271. log.Stream,
  1272. log.OpenAIWSMode,
  1273. duration,
  1274. firstToken,
  1275. userAgent,
  1276. ipAddress,
  1277. log.ImageCount,
  1278. imageSize,
  1279. imageInputSize,
  1280. imageOutputSize,
  1281. imageSizeSource,
  1282. imageSizeBreakdown,
  1283. log.VideoCount,
  1284. videoResolution,
  1285. videoDurationSeconds,
  1286. serviceTier,
  1287. reasoningEffort,
  1288. requestedReasoningEffort,
  1289. inboundEndpoint,
  1290. upstreamEndpoint,
  1291. log.CacheTTLOverridden,
  1292. log.LongContextBillingApplied,
  1293. channelID,
  1294. modelMappingChain,
  1295. billingTier,
  1296. billingMode,
  1297. log.AccountStatsCost, // account_stats_cost
  1298. upstreamRequestID, // upstream_request_id
  1299. sessionID, // session_id
  1300. log.NativeCompactionV2,
  1301. createdAt,
  1302. },
  1303. }
  1304. }
  1305. func usageLogBatchKey(requestID string, apiKeyID int64) string {
  1306. return requestID + "\x1f" + strconv.FormatInt(apiKeyID, 10)
  1307. }
  1308. func sendUsageLogCreateResult(ch chan usageLogCreateResult, res usageLogCreateResult) {
  1309. if ch == nil {
  1310. return
  1311. }
  1312. select {
  1313. case ch <- res:
  1314. default:
  1315. }
  1316. }
  1317. func (r *usageLogRepository) bestEffortRecentKey(requestID string, apiKeyID int64) (string, bool) {
  1318. requestID = strings.TrimSpace(requestID)
  1319. if requestID == "" || r == nil || r.bestEffortRecent == nil {
  1320. return "", false
  1321. }
  1322. return usageLogBatchKey(requestID, apiKeyID), true
  1323. }