forked from james-6-23/codex2api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpostgres.go
More file actions
2447 lines (2237 loc) · 87.2 KB
/
postgres.go
File metadata and controls
2447 lines (2237 loc) · 87.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package database
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"strings"
"sync"
"time"
_ "github.com/lib/pq"
_ "modernc.org/sqlite"
)
// AccountRow 数据库中的账号行
type AccountRow struct {
ID int64
Name string
Platform string
Type string
Credentials map[string]interface{}
ProxyURL string
Status string
CooldownReason string
CooldownUntil sql.NullTime
ErrorMessage string
Enabled bool
Locked bool
ScoreBiasOverride sql.NullInt64
BaseConcurrencyOverride sql.NullInt64
CreatedAt time.Time
UpdatedAt time.Time
}
type OptionalInt64Slice struct {
Set bool
Values []int64
}
// GetCredential 从 credentials JSONB 获取字符串字段
func (a *AccountRow) GetCredential(key string) string {
if a.Credentials == nil {
return ""
}
v, ok := a.Credentials[key]
if !ok || v == nil {
return ""
}
switch val := v.(type) {
case string:
return val
case float64:
return fmt.Sprintf("%v", val)
default:
return ""
}
}
func (a *AccountRow) GetCredentialInt64Slice(key string) []int64 {
if a.Credentials == nil {
return []int64{}
}
value, ok := a.Credentials[key]
if !ok {
return []int64{}
}
return int64SliceFromValue(value)
}
// DB PostgreSQL 数据库操作
type DB struct {
conn *sql.DB
driver string
// 使用日志批量写入缓冲
logBuf []usageLogEntry
logMu sync.Mutex
logStop chan struct{}
logWg sync.WaitGroup
// 预分配日志缓冲以减少内存分配
logBufCap int
}
// usageLogEntry 日志缓冲条目
type usageLogEntry struct {
AccountID int64
Endpoint string
Model string
EffectiveModel string
PromptTokens int
CompletionTokens int
TotalTokens int
StatusCode int
DurationMs int
InputTokens int
OutputTokens int
ReasoningTokens int
FirstTokenMs int
ReasoningEffort string
InboundEndpoint string
UpstreamEndpoint string
Stream bool
CachedTokens int
ServiceTier string
APIKeyID int64
APIKeyName string
APIKeyMasked string
ImageCount int
ImageWidth int
ImageHeight int
ImageBytes int
ImageFormat string
ImageSize string
AccountBilled float64
UserBilled float64
}
// New 创建数据库连接并自动建表。
func New(driver string, dsn string) (*DB, error) {
driver = normalizeDriver(driver)
driverName := driver
if driver == "sqlite" {
driverName = "sqlite"
}
conn, err := sql.Open(driverName, dsn)
if err != nil {
return nil, fmt.Errorf("连接数据库失败: %w", err)
}
// ==================== 连接池优化 ====================
if driver == "sqlite" {
conn.SetMaxOpenConns(1)
conn.SetMaxIdleConns(1)
} else {
// 高并发场景:大量 RT 刷新 + 前端查询 + 使用日志写入 并行
conn.SetMaxOpenConns(100) // 增加最大打开连接数以处理更高并发
conn.SetMaxIdleConns(50) // 增加空闲连接数以保持热连接
conn.SetConnMaxLifetime(60 * time.Minute) // 增加连接最大生存时间
conn.SetConnMaxIdleTime(30 * time.Minute) // 增加空闲连接最大闲置时间
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := conn.PingContext(ctx); err != nil {
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
}
db := &DB{
conn: conn,
driver: driver,
logStop: make(chan struct{}),
logBufCap: 128,
}
if db.isSQLite() {
if err := db.configureSQLite(ctx); err != nil {
return nil, fmt.Errorf("配置 SQLite 失败: %w", err)
}
} else {
// PostgreSQL: 统一会话时区为 UTC,确保 NOW() 和时间字面量一致
if _, err := conn.ExecContext(ctx, "SET timezone = 'UTC'"); err != nil {
return nil, fmt.Errorf("设置数据库时区失败: %w", err)
}
}
if err := db.migrate(ctx); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}
// 启动批量写入后台协程
db.startLogFlusher()
baselineInsert := `
INSERT INTO usage_stats_baseline (id) VALUES (1) ON CONFLICT DO NOTHING
`
if db.isSQLite() {
baselineInsert = `
INSERT OR IGNORE INTO usage_stats_baseline (id) VALUES (1)
`
}
_, err = db.conn.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS usage_stats_baseline (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
total_requests BIGINT NOT NULL DEFAULT 0,
total_tokens BIGINT NOT NULL DEFAULT 0,
prompt_tokens BIGINT NOT NULL DEFAULT 0,
completion_tokens BIGINT NOT NULL DEFAULT 0,
cached_tokens BIGINT NOT NULL DEFAULT 0,
account_billed DOUBLE PRECISION NOT NULL DEFAULT 0,
user_billed DOUBLE PRECISION NOT NULL DEFAULT 0
)
`)
if err != nil {
return nil, fmt.Errorf("创建 usage_stats_baseline 表失败: %w", err)
}
// 确保 baseline 行存在
_, err = db.conn.ExecContext(ctx, baselineInsert)
if err != nil {
return nil, fmt.Errorf("初始化 usage_stats_baseline 失败: %w", err)
}
if err := db.ensureUsageStatsBaselineBillingColumns(ctx); err != nil {
return nil, err
}
return db, nil
}
func (db *DB) ensureUsageStatsBaselineBillingColumns(ctx context.Context) error {
if db.isSQLite() {
columns, err := db.sqliteTableColumns(ctx, "usage_stats_baseline")
if err != nil {
return err
}
for _, column := range []struct {
name string
def string
}{
{name: "account_billed", def: "REAL NOT NULL DEFAULT 0"},
{name: "user_billed", def: "REAL NOT NULL DEFAULT 0"},
} {
if _, ok := columns[column.name]; ok {
continue
}
if _, err := db.conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE usage_stats_baseline ADD COLUMN %s %s", column.name, column.def)); err != nil {
return err
}
}
return nil
}
_, err := db.conn.ExecContext(ctx, `
ALTER TABLE usage_stats_baseline ADD COLUMN IF NOT EXISTS account_billed DOUBLE PRECISION NOT NULL DEFAULT 0;
ALTER TABLE usage_stats_baseline ADD COLUMN IF NOT EXISTS user_billed DOUBLE PRECISION NOT NULL DEFAULT 0;
`)
return err
}
// Close 关闭数据库连接
func (db *DB) Close() error {
// 停止批量写入并刷完缓冲
close(db.logStop)
db.logWg.Wait()
db.flushLogs() // 最后一次 flush
return db.conn.Close()
}
// migrate 自动建表
func (db *DB) migrate(ctx context.Context) error {
if db.isSQLite() {
return db.migrateSQLite(ctx)
}
query := `
CREATE TABLE IF NOT EXISTS accounts (
id SERIAL PRIMARY KEY,
name VARCHAR(255) DEFAULT '',
platform VARCHAR(50) DEFAULT 'openai',
type VARCHAR(50) DEFAULT 'oauth',
credentials JSONB NOT NULL DEFAULT '{}',
proxy_url VARCHAR(500) DEFAULT '',
status VARCHAR(50) DEFAULT 'active',
error_message TEXT DEFAULT '',
deleted_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS cooldown_reason VARCHAR(50) DEFAULT '';
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS cooldown_until TIMESTAMPTZ NULL;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS enabled BOOLEAN DEFAULT TRUE;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS locked BOOLEAN DEFAULT FALSE;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS score_bias_override INT NULL;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS base_concurrency_override INT NULL;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ NULL;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS image_quota_remaining INT NULL;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS image_quota_total INT NULL;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS today_used_count INT DEFAULT 0;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS image_quota_reset_at TIMESTAMPTZ NULL;
UPDATE accounts
SET status = 'deleted',
error_message = '',
cooldown_reason = '',
cooldown_until = NULL,
deleted_at = COALESCE(deleted_at, updated_at, NOW()),
updated_at = NOW()
WHERE status <> 'deleted' AND COALESCE(error_message, '') = 'deleted';
CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status);
CREATE INDEX IF NOT EXISTS idx_accounts_platform ON accounts(platform);
CREATE INDEX IF NOT EXISTS idx_accounts_cooldown_until ON accounts(cooldown_until);
CREATE TABLE IF NOT EXISTS usage_logs (
id SERIAL PRIMARY KEY,
account_id INT DEFAULT 0,
endpoint VARCHAR(100) DEFAULT '',
model VARCHAR(100) DEFAULT '',
prompt_tokens INT DEFAULT 0,
completion_tokens INT DEFAULT 0,
total_tokens INT DEFAULT 0,
status_code INT DEFAULT 0,
duration_ms INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usage_logs_created_at ON usage_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_usage_logs_account_id ON usage_logs(account_id);
CREATE INDEX IF NOT EXISTS idx_usage_logs_created_status ON usage_logs(created_at, status_code);
CREATE INDEX IF NOT EXISTS idx_usage_logs_account_status ON usage_logs(account_id, status_code);
-- 增强字段(向后兼容 ALTER)
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS input_tokens INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS output_tokens INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS reasoning_tokens INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS first_token_ms INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS reasoning_effort VARCHAR(20) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS effective_model VARCHAR(100) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS inbound_endpoint VARCHAR(100) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS upstream_endpoint VARCHAR(100) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS stream BOOLEAN DEFAULT false;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS cached_tokens INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS service_tier VARCHAR(20) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS api_key_id INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS api_key_name VARCHAR(255) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS api_key_masked VARCHAR(64) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_count INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_width INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_height INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_bytes INT DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_format VARCHAR(20) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_size VARCHAR(32) DEFAULT '';
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS account_billed DOUBLE PRECISION DEFAULT 0;
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS user_billed DOUBLE PRECISION DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_usage_logs_api_key_created_at ON usage_logs(api_key_id, created_at);
CREATE TABLE IF NOT EXISTS api_keys (
id SERIAL PRIMARY KEY,
name VARCHAR(255) DEFAULT '',
key VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS system_settings (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
max_concurrency INT DEFAULT 2,
global_rpm INT DEFAULT 0,
test_model VARCHAR(100) DEFAULT 'gpt-5.4',
test_concurrency INT DEFAULT 50,
proxy_url VARCHAR(500) DEFAULT '',
pg_max_conns INT DEFAULT 50,
redis_pool_size INT DEFAULT 30,
auto_clean_unauthorized BOOLEAN DEFAULT FALSE,
auto_clean_rate_limited BOOLEAN DEFAULT FALSE,
background_refresh_interval_minutes INT DEFAULT 2,
usage_probe_max_age_minutes INT DEFAULT 10,
recovery_probe_interval_minutes INT DEFAULT 30
);
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS pg_max_conns INT DEFAULT 50;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS redis_pool_size INT DEFAULT 30;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_unauthorized BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_rate_limited BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS admin_secret VARCHAR(255) DEFAULT '';
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_full_usage BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS proxy_pool_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS fast_scheduler_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS max_retries INT DEFAULT 2;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS allow_remote_migration BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_error BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS auto_clean_expired BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS model_mapping TEXT DEFAULT '{}';
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS background_refresh_interval_minutes INT DEFAULT 2;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS usage_probe_max_age_minutes INT DEFAULT 10;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS recovery_probe_interval_minutes INT DEFAULT 30;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS resin_url TEXT DEFAULT '';
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS resin_platform_name TEXT DEFAULT '';
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_mode VARCHAR(20) DEFAULT 'monitor';
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_threshold INT DEFAULT 50;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_strict_threshold INT DEFAULT 90;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_log_matches BOOLEAN DEFAULT TRUE;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_max_text_length INT DEFAULT 81920;
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_sensitive_words TEXT DEFAULT '';
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_custom_patterns TEXT DEFAULT '[]';
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS prompt_filter_disabled_patterns TEXT DEFAULT '[]';
CREATE TABLE IF NOT EXISTS prompt_filter_logs (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(),
source VARCHAR(50) DEFAULT '',
endpoint VARCHAR(100) DEFAULT '',
model VARCHAR(100) DEFAULT '',
action VARCHAR(20) DEFAULT '',
mode VARCHAR(20) DEFAULT '',
score INT DEFAULT 0,
threshold_value INT DEFAULT 0,
matched_patterns TEXT DEFAULT '[]',
text_preview TEXT DEFAULT '',
api_key_id INT DEFAULT 0,
api_key_name VARCHAR(255) DEFAULT '',
api_key_masked VARCHAR(64) DEFAULT '',
client_ip VARCHAR(64) DEFAULT '',
error_code VARCHAR(100) DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_prompt_filter_logs_created_at ON prompt_filter_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_prompt_filter_logs_action_created_at ON prompt_filter_logs(action, created_at);
CREATE TABLE IF NOT EXISTS model_registry (
id VARCHAR(100) PRIMARY KEY,
enabled BOOLEAN DEFAULT TRUE,
category VARCHAR(50) DEFAULT 'codex',
source VARCHAR(50) DEFAULT 'manual',
pro_only BOOLEAN DEFAULT FALSE,
api_key_auth_available BOOLEAN DEFAULT TRUE,
last_seen_at TIMESTAMPTZ NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS model_registry_sync (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
source_url TEXT DEFAULT '',
last_synced_at TIMESTAMPTZ NULL
);
CREATE TABLE IF NOT EXISTS proxies (
id SERIAL PRIMARY KEY,
url VARCHAR(500) NOT NULL UNIQUE,
label VARCHAR(255) DEFAULT '',
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE proxies ADD COLUMN IF NOT EXISTS test_ip VARCHAR(100) DEFAULT '';
ALTER TABLE proxies ADD COLUMN IF NOT EXISTS test_location VARCHAR(255) DEFAULT '';
ALTER TABLE proxies ADD COLUMN IF NOT EXISTS test_latency_ms INT DEFAULT 0;
CREATE TABLE IF NOT EXISTS account_events (
id SERIAL PRIMARY KEY,
account_id INT NOT NULL DEFAULT 0,
event_type VARCHAR(20) NOT NULL,
source VARCHAR(30) DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_account_events_created ON account_events(created_at);
CREATE INDEX IF NOT EXISTS idx_account_events_type_created ON account_events(event_type, created_at);
CREATE TABLE IF NOT EXISTS image_prompt_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL DEFAULT '',
prompt TEXT NOT NULL DEFAULT '',
model VARCHAR(100) DEFAULT '',
size VARCHAR(32) DEFAULT '',
quality VARCHAR(32) DEFAULT '',
output_format VARCHAR(32) DEFAULT '',
background VARCHAR(32) DEFAULT '',
style VARCHAR(64) DEFAULT '',
tags TEXT NOT NULL DEFAULT '[]',
favorite BOOLEAN DEFAULT FALSE,
usage_count INT DEFAULT 0,
last_used_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_image_prompt_templates_updated ON image_prompt_templates(updated_at);
CREATE INDEX IF NOT EXISTS idx_image_prompt_templates_favorite ON image_prompt_templates(favorite, updated_at);
CREATE TABLE IF NOT EXISTS image_generation_jobs (
id SERIAL PRIMARY KEY,
status VARCHAR(32) NOT NULL DEFAULT 'queued',
prompt TEXT NOT NULL DEFAULT '',
params_json TEXT NOT NULL DEFAULT '{}',
api_key_id INT DEFAULT 0,
api_key_name VARCHAR(255) DEFAULT '',
api_key_masked VARCHAR(64) DEFAULT '',
error_message TEXT DEFAULT '',
duration_ms INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
started_at TIMESTAMPTZ NULL,
completed_at TIMESTAMPTZ NULL
);
CREATE INDEX IF NOT EXISTS idx_image_generation_jobs_created ON image_generation_jobs(created_at);
CREATE INDEX IF NOT EXISTS idx_image_generation_jobs_status ON image_generation_jobs(status, created_at);
CREATE TABLE IF NOT EXISTS image_assets (
id SERIAL PRIMARY KEY,
job_id INT NOT NULL DEFAULT 0,
template_id INT DEFAULT 0,
filename VARCHAR(255) NOT NULL DEFAULT '',
storage_path TEXT NOT NULL DEFAULT '',
mime_type VARCHAR(100) NOT NULL DEFAULT '',
bytes INT DEFAULT 0,
width INT DEFAULT 0,
height INT DEFAULT 0,
model VARCHAR(100) DEFAULT '',
requested_size VARCHAR(32) DEFAULT '',
actual_size VARCHAR(32) DEFAULT '',
quality VARCHAR(32) DEFAULT '',
output_format VARCHAR(32) DEFAULT '',
revised_prompt TEXT DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_image_assets_created ON image_assets(created_at);
CREATE INDEX IF NOT EXISTS idx_image_assets_job_id ON image_assets(job_id);
`
_, err := db.conn.ExecContext(ctx, query)
if err != nil {
return err
}
// 独立长超时:将已有 TIMESTAMP 列迁移为 TIMESTAMPTZ(大表 ALTER COLUMN TYPE 可能较慢)
migrateQuery := `
DO $$
DECLARE
_tbl TEXT;
_col TEXT;
_rec RECORD;
BEGIN
FOR _rec IN
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = current_schema()
AND data_type = 'timestamp without time zone'
AND table_name IN ('accounts', 'usage_logs', 'api_keys', 'proxies', 'account_events')
LOOP
EXECUTE format(
'ALTER TABLE %I ALTER COLUMN %I TYPE TIMESTAMPTZ USING %I AT TIME ZONE current_setting(''TIMEZONE'')',
_rec.table_name, _rec.column_name, _rec.column_name
);
END LOOP;
END $$;
`
migrateCtx, migrateCancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer migrateCancel()
_, err = db.conn.ExecContext(migrateCtx, migrateQuery)
return err
}
// ==================== API Keys ====================
// APIKeyRow API 密钥行
type APIKeyRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
Key string `json:"key"`
CreatedAt time.Time `json:"created_at"`
}
// ListAPIKeys 获取所有 API 密钥
func (db *DB) ListAPIKeys(ctx context.Context) ([]*APIKeyRow, error) {
rows, err := db.conn.QueryContext(ctx, `SELECT id, name, key, created_at FROM api_keys ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var keys []*APIKeyRow
for rows.Next() {
k := &APIKeyRow{}
var createdAtRaw interface{}
if err := rows.Scan(&k.ID, &k.Name, &k.Key, &createdAtRaw); err != nil {
return nil, err
}
k.CreatedAt, err = parseDBTimeValue(createdAtRaw)
if err != nil {
return nil, err
}
keys = append(keys, k)
}
return keys, rows.Err()
}
// InsertAPIKey 插入新 API 密钥
func (db *DB) InsertAPIKey(ctx context.Context, name, key string) (int64, error) {
return db.insertRowID(ctx,
`INSERT INTO api_keys (name, key) VALUES ($1, $2) RETURNING id`,
`INSERT INTO api_keys (name, key) VALUES ($1, $2)`,
name, key,
)
}
// ==================== System Settings ====================
// SystemSettings 运行时设置项
type SystemSettings struct {
MaxConcurrency int
GlobalRPM int
TestModel string
TestConcurrency int
ProxyURL string
PgMaxConns int
RedisPoolSize int
AutoCleanUnauthorized bool
AutoCleanRateLimited bool
AdminSecret string
AutoCleanFullUsage bool
AutoCleanError bool
AutoCleanExpired bool
ProxyPoolEnabled bool
FastSchedulerEnabled bool
MaxRetries int
AllowRemoteMigration bool
ModelMapping string // JSON: {"anthropic_model": "codex_model", ...}
BackgroundRefreshIntervalMinutes int
UsageProbeMaxAgeMinutes int
RecoveryProbeIntervalMinutes int
ResinURL string // Resin 代理池地址(含 Token),例如 http://127.0.0.1:2260/my-token
ResinPlatformName string // Resin 平台标识,例如 codex2api
PromptFilterEnabled bool
PromptFilterMode string
PromptFilterThreshold int
PromptFilterStrictThreshold int
PromptFilterLogMatches bool
PromptFilterMaxTextLength int
PromptFilterSensitiveWords string
PromptFilterCustomPatterns string
PromptFilterDisabledPatterns string
}
// GetSystemSettings 加载全局设置
func (db *DB) GetSystemSettings(ctx context.Context) (*SystemSettings, error) {
s := &SystemSettings{}
err := db.conn.QueryRowContext(ctx, `
SELECT max_concurrency, global_rpm, test_model, test_concurrency, proxy_url, pg_max_conns, redis_pool_size,
auto_clean_unauthorized, auto_clean_rate_limited, COALESCE(admin_secret, ''), COALESCE(auto_clean_full_usage, false),
COALESCE(proxy_pool_enabled, false),
COALESCE(fast_scheduler_enabled, false),
COALESCE(max_retries, 2),
COALESCE(allow_remote_migration, false),
COALESCE(auto_clean_error, false),
COALESCE(auto_clean_expired, false),
COALESCE(model_mapping, '{}'),
COALESCE(background_refresh_interval_minutes, 2),
COALESCE(usage_probe_max_age_minutes, 10),
COALESCE(recovery_probe_interval_minutes, 30),
COALESCE(resin_url, ''),
COALESCE(resin_platform_name, ''),
COALESCE(prompt_filter_enabled, false),
COALESCE(prompt_filter_mode, 'monitor'),
COALESCE(prompt_filter_threshold, 50),
COALESCE(prompt_filter_strict_threshold, 90),
COALESCE(prompt_filter_log_matches, true),
COALESCE(prompt_filter_max_text_length, 81920),
COALESCE(prompt_filter_sensitive_words, ''),
COALESCE(prompt_filter_custom_patterns, '[]'),
COALESCE(prompt_filter_disabled_patterns, '[]')
FROM system_settings WHERE id = 1
`).Scan(
&s.MaxConcurrency, &s.GlobalRPM, &s.TestModel, &s.TestConcurrency, &s.ProxyURL, &s.PgMaxConns, &s.RedisPoolSize,
&s.AutoCleanUnauthorized, &s.AutoCleanRateLimited, &s.AdminSecret, &s.AutoCleanFullUsage,
&s.ProxyPoolEnabled, &s.FastSchedulerEnabled, &s.MaxRetries, &s.AllowRemoteMigration,
&s.AutoCleanError, &s.AutoCleanExpired, &s.ModelMapping,
&s.BackgroundRefreshIntervalMinutes, &s.UsageProbeMaxAgeMinutes, &s.RecoveryProbeIntervalMinutes,
&s.ResinURL, &s.ResinPlatformName,
&s.PromptFilterEnabled, &s.PromptFilterMode, &s.PromptFilterThreshold, &s.PromptFilterStrictThreshold,
&s.PromptFilterLogMatches, &s.PromptFilterMaxTextLength, &s.PromptFilterSensitiveWords,
&s.PromptFilterCustomPatterns, &s.PromptFilterDisabledPatterns,
)
if err == sql.ErrNoRows {
return nil, nil
}
return s, err
}
// UpdateSystemSettings 更新全局设置(upsert:无行时自动插入)
func (db *DB) UpdateSystemSettings(ctx context.Context, s *SystemSettings) error {
_, err := db.conn.ExecContext(ctx, `
INSERT INTO system_settings (
id, max_concurrency, global_rpm, test_model, test_concurrency, proxy_url, pg_max_conns, redis_pool_size,
auto_clean_unauthorized, auto_clean_rate_limited, admin_secret, auto_clean_full_usage, proxy_pool_enabled,
fast_scheduler_enabled, max_retries, allow_remote_migration, auto_clean_error, auto_clean_expired, model_mapping,
background_refresh_interval_minutes, usage_probe_max_age_minutes, recovery_probe_interval_minutes,
resin_url, resin_platform_name, prompt_filter_enabled, prompt_filter_mode, prompt_filter_threshold,
prompt_filter_strict_threshold, prompt_filter_log_matches, prompt_filter_max_text_length,
prompt_filter_sensitive_words, prompt_filter_custom_patterns, prompt_filter_disabled_patterns
)
VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)
ON CONFLICT (id) DO UPDATE SET
max_concurrency = EXCLUDED.max_concurrency,
global_rpm = EXCLUDED.global_rpm,
test_model = EXCLUDED.test_model,
test_concurrency = EXCLUDED.test_concurrency,
proxy_url = EXCLUDED.proxy_url,
pg_max_conns = EXCLUDED.pg_max_conns,
redis_pool_size = EXCLUDED.redis_pool_size,
auto_clean_unauthorized = EXCLUDED.auto_clean_unauthorized,
auto_clean_rate_limited = EXCLUDED.auto_clean_rate_limited,
admin_secret = EXCLUDED.admin_secret,
auto_clean_full_usage = EXCLUDED.auto_clean_full_usage,
proxy_pool_enabled = EXCLUDED.proxy_pool_enabled,
fast_scheduler_enabled = EXCLUDED.fast_scheduler_enabled,
max_retries = EXCLUDED.max_retries,
allow_remote_migration = EXCLUDED.allow_remote_migration,
auto_clean_error = EXCLUDED.auto_clean_error,
auto_clean_expired = EXCLUDED.auto_clean_expired,
model_mapping = EXCLUDED.model_mapping,
background_refresh_interval_minutes = EXCLUDED.background_refresh_interval_minutes,
usage_probe_max_age_minutes = EXCLUDED.usage_probe_max_age_minutes,
recovery_probe_interval_minutes = EXCLUDED.recovery_probe_interval_minutes,
resin_url = EXCLUDED.resin_url,
resin_platform_name = EXCLUDED.resin_platform_name,
prompt_filter_enabled = EXCLUDED.prompt_filter_enabled,
prompt_filter_mode = EXCLUDED.prompt_filter_mode,
prompt_filter_threshold = EXCLUDED.prompt_filter_threshold,
prompt_filter_strict_threshold = EXCLUDED.prompt_filter_strict_threshold,
prompt_filter_log_matches = EXCLUDED.prompt_filter_log_matches,
prompt_filter_max_text_length = EXCLUDED.prompt_filter_max_text_length,
prompt_filter_sensitive_words = EXCLUDED.prompt_filter_sensitive_words,
prompt_filter_custom_patterns = EXCLUDED.prompt_filter_custom_patterns,
prompt_filter_disabled_patterns = EXCLUDED.prompt_filter_disabled_patterns
`, s.MaxConcurrency, s.GlobalRPM, s.TestModel, s.TestConcurrency, s.ProxyURL, s.PgMaxConns, s.RedisPoolSize,
s.AutoCleanUnauthorized, s.AutoCleanRateLimited, s.AdminSecret, s.AutoCleanFullUsage, s.ProxyPoolEnabled,
s.FastSchedulerEnabled, s.MaxRetries, s.AllowRemoteMigration, s.AutoCleanError, s.AutoCleanExpired, s.ModelMapping,
s.BackgroundRefreshIntervalMinutes, s.UsageProbeMaxAgeMinutes, s.RecoveryProbeIntervalMinutes,
s.ResinURL, s.ResinPlatformName, s.PromptFilterEnabled, s.PromptFilterMode, s.PromptFilterThreshold,
s.PromptFilterStrictThreshold, s.PromptFilterLogMatches, s.PromptFilterMaxTextLength,
s.PromptFilterSensitiveWords, s.PromptFilterCustomPatterns, s.PromptFilterDisabledPatterns)
return err
}
// DeleteAPIKey 删除 API 密钥
func (db *DB) DeleteAPIKey(ctx context.Context, id int64) error {
_, err := db.conn.ExecContext(ctx, `DELETE FROM api_keys WHERE id = $1`, id)
return err
}
// GetAllAPIKeyValues 获取所有密钥值(用于鉴权)
func (db *DB) GetAllAPIKeyValues(ctx context.Context) ([]string, error) {
rows, err := db.conn.QueryContext(ctx, `SELECT key FROM api_keys`)
if err != nil {
return nil, err
}
defer rows.Close()
var keys []string
for rows.Next() {
var k string
if err := rows.Scan(&k); err != nil {
return nil, err
}
keys = append(keys, k)
}
return keys, rows.Err()
}
// ==================== Proxies ====================
// ProxyRow 代理行
type ProxyRow struct {
ID int64 `json:"id"`
URL string `json:"url"`
Label string `json:"label"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
TestIP string `json:"test_ip"`
TestLocation string `json:"test_location"`
TestLatencyMs int `json:"test_latency_ms"`
}
// ListProxies 获取所有代理
func (db *DB) ListProxies(ctx context.Context) ([]*ProxyRow, error) {
rows, err := db.conn.QueryContext(ctx, `SELECT id, url, label, enabled, created_at, COALESCE(test_ip,''), COALESCE(test_location,''), COALESCE(test_latency_ms,0) FROM proxies ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var proxies []*ProxyRow
for rows.Next() {
p := &ProxyRow{}
var createdAtRaw interface{}
if err := rows.Scan(&p.ID, &p.URL, &p.Label, &p.Enabled, &createdAtRaw, &p.TestIP, &p.TestLocation, &p.TestLatencyMs); err != nil {
return nil, err
}
p.CreatedAt, err = parseDBTimeValue(createdAtRaw)
if err != nil {
return nil, err
}
proxies = append(proxies, p)
}
return proxies, rows.Err()
}
// ListEnabledProxies 获取已启用的代理
func (db *DB) ListEnabledProxies(ctx context.Context) ([]*ProxyRow, error) {
query := `SELECT id, url, label, enabled, created_at, COALESCE(test_ip,''), COALESCE(test_location,''), COALESCE(test_latency_ms,0) FROM proxies WHERE enabled = true ORDER BY id`
if db.isSQLite() {
query = `SELECT id, url, label, enabled, created_at, COALESCE(test_ip,''), COALESCE(test_location,''), COALESCE(test_latency_ms,0) FROM proxies WHERE enabled = 1 ORDER BY id`
}
rows, err := db.conn.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var proxies []*ProxyRow
for rows.Next() {
p := &ProxyRow{}
var createdAtRaw interface{}
if err := rows.Scan(&p.ID, &p.URL, &p.Label, &p.Enabled, &createdAtRaw, &p.TestIP, &p.TestLocation, &p.TestLatencyMs); err != nil {
return nil, err
}
p.CreatedAt, err = parseDBTimeValue(createdAtRaw)
if err != nil {
return nil, err
}
proxies = append(proxies, p)
}
return proxies, rows.Err()
}
// InsertProxy 插入单个代理
func (db *DB) InsertProxy(ctx context.Context, url, label string) (int64, error) {
return db.insertRowID(ctx,
`INSERT INTO proxies (url, label) VALUES ($1, $2) ON CONFLICT (url) DO NOTHING RETURNING id`,
`INSERT INTO proxies (url, label) VALUES ($1, $2) ON CONFLICT(url) DO NOTHING`,
url, label,
)
}
// InsertProxies 批量插入代理(跳过已存在的)
func (db *DB) InsertProxies(ctx context.Context, urls []string, label string) (int, error) {
inserted := 0
for _, u := range urls {
if db.isSQLite() {
res, err := db.conn.ExecContext(ctx, `INSERT INTO proxies (url, label) VALUES ($1, $2) ON CONFLICT(url) DO NOTHING`, u, label)
if err != nil {
continue
}
affected, _ := res.RowsAffected()
if affected > 0 {
inserted++
}
continue
}
var id int64
err := db.conn.QueryRowContext(ctx,
`INSERT INTO proxies (url, label) VALUES ($1, $2) ON CONFLICT (url) DO NOTHING RETURNING id`, u, label).Scan(&id)
if err == nil {
inserted++
}
}
return inserted, nil
}
// DeleteProxy 删除单个代理
func (db *DB) DeleteProxy(ctx context.Context, id int64) error {
_, err := db.conn.ExecContext(ctx, `DELETE FROM proxies WHERE id = $1`, id)
return err
}
// DeleteProxies 批量删除代理
func (db *DB) DeleteProxies(ctx context.Context, ids []int64) (int, error) {
if len(ids) == 0 {
return 0, nil
}
// 构建 IN 子句
args := make([]interface{}, len(ids))
placeholders := make([]string, len(ids))
for i, id := range ids {
args[i] = id
placeholders[i] = fmt.Sprintf("$%d", i+1)
}
query := fmt.Sprintf("DELETE FROM proxies WHERE id IN (%s)", strings.Join(placeholders, ","))
res, err := db.conn.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
affected, _ := res.RowsAffected()
return int(affected), nil
}
// UpdateProxy 更新代理
func (db *DB) UpdateProxy(ctx context.Context, id int64, label *string, enabled *bool) error {
if label != nil {
if _, err := db.conn.ExecContext(ctx, `UPDATE proxies SET label = $1 WHERE id = $2`, *label, id); err != nil {
return err
}
}
if enabled != nil {
if _, err := db.conn.ExecContext(ctx, `UPDATE proxies SET enabled = $1 WHERE id = $2`, *enabled, id); err != nil {
return err
}
}
return nil
}
// UpdateProxyTestResult 更新代理测试结果
func (db *DB) UpdateProxyTestResult(ctx context.Context, id int64, ip, location string, latencyMs int) error {
_, err := db.conn.ExecContext(ctx,
`UPDATE proxies SET test_ip = $1, test_location = $2, test_latency_ms = $3 WHERE id = $4`,
ip, location, latencyMs, id)
return err
}
// ==================== Usage Logs(批量写入) ====================
// UsageLog 请求日志行
type UsageLog struct {
ID int64 `json:"id"`
AccountID int64 `json:"account_id"`
Endpoint string `json:"endpoint"`
Model string `json:"model"`
EffectiveModel string `json:"effective_model"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
StatusCode int `json:"status_code"`
DurationMs int `json:"duration_ms"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
ReasoningTokens int `json:"reasoning_tokens"`
FirstTokenMs int `json:"first_token_ms"`
ReasoningEffort string `json:"reasoning_effort"`
InboundEndpoint string `json:"inbound_endpoint"`
UpstreamEndpoint string `json:"upstream_endpoint"`
Stream bool `json:"stream"`
CachedTokens int `json:"cached_tokens"`
ServiceTier string `json:"service_tier"`
APIKeyID int64 `json:"api_key_id"`
APIKeyName string `json:"api_key_name"`
APIKeyMasked string `json:"api_key_masked"`
ImageCount int `json:"image_count"`
ImageWidth int `json:"image_width"`
ImageHeight int `json:"image_height"`
ImageBytes int `json:"image_bytes"`
ImageFormat string `json:"image_format"`
ImageSize string `json:"image_size"`
AccountEmail string `json:"account_email"`
CreatedAt time.Time `json:"created_at"`
AccountBilled float64 `json:"account_billed"`
UserBilled float64 `json:"user_billed"`
InputCost float64 `json:"input_cost"`
OutputCost float64 `json:"output_cost"`
CacheReadCost float64 `json:"cache_read_cost"`
TotalCost float64 `json:"total_cost"`
InputPrice float64 `json:"input_price_per_mtoken"`
OutputPrice float64 `json:"output_price_per_mtoken"`
CacheReadPrice float64 `json:"cache_read_price_per_mtoken"`
RateMultiplier float64 `json:"rate_multiplier"`
}
// InsertUsageLog 将日志追加到内存缓冲(非阻塞)
func (db *DB) InsertUsageLog(ctx context.Context, log *UsageLogInput) error {
// 计算计费金额(基于 input/output tokens 和模型)
// 使用 EffectiveModel 作为计费模型(如果有映射则使用映射后的模型)
billingModel := log.EffectiveModel
if billingModel == "" {
billingModel = log.Model
}
// 计算账号计费金额(标准费用)
accountBilled := calculateCost(log.InputTokens, log.OutputTokens, log.CachedTokens, billingModel, log.ServiceTier)
// 用户计费金额与账号计费金额相同(简化版,未来可支持倍率)
userBilled := accountBilled
db.logMu.Lock()
db.logBuf = append(db.logBuf, usageLogEntry{
AccountID: log.AccountID,
Endpoint: log.Endpoint,
Model: log.Model,
EffectiveModel: log.EffectiveModel,
PromptTokens: log.PromptTokens,
CompletionTokens: log.CompletionTokens,
TotalTokens: log.TotalTokens,
StatusCode: log.StatusCode,
DurationMs: log.DurationMs,
InputTokens: log.InputTokens,
OutputTokens: log.OutputTokens,
ReasoningTokens: log.ReasoningTokens,
FirstTokenMs: log.FirstTokenMs,
ReasoningEffort: log.ReasoningEffort,
InboundEndpoint: log.InboundEndpoint,
UpstreamEndpoint: log.UpstreamEndpoint,
Stream: log.Stream,
CachedTokens: log.CachedTokens,
ServiceTier: log.ServiceTier,
APIKeyID: log.APIKeyID,
APIKeyName: log.APIKeyName,
APIKeyMasked: log.APIKeyMasked,
ImageCount: log.ImageCount,
ImageWidth: log.ImageWidth,
ImageHeight: log.ImageHeight,
ImageBytes: log.ImageBytes,
ImageFormat: log.ImageFormat,
ImageSize: log.ImageSize,
AccountBilled: accountBilled,
UserBilled: userBilled,
})
bufLen := len(db.logBuf)
db.logMu.Unlock()
// 增加触发 flush 的阈值,减少 flush 频率
if bufLen >= 200 {
go db.flushLogs()
}
return nil
}
// UsageLogInput 日志写入参数