mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Compare commits
1182
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99efaa477f | ||
|
|
d9d4333795 | ||
|
|
458b92abd4 | ||
|
|
5106065716 | ||
|
|
46f5f5e8ba | ||
|
|
2529371bab | ||
|
|
43c4c882fd | ||
|
|
719013053a | ||
|
|
f3dc0835df | ||
|
|
5637f469fa | ||
|
|
34fe845f5a | ||
|
|
fa49647c6d | ||
|
|
9eb394e3e9 | ||
|
|
2d8a386811 | ||
|
|
bb97ab364a | ||
|
|
7225538358 | ||
|
|
0cecf3c367 | ||
|
|
1aa34d478d | ||
|
|
1b1039f4fd | ||
|
|
d6adebd22d | ||
|
|
53158587a6 | ||
|
|
ad05eda157 | ||
|
|
fdce5b8d01 | ||
|
|
00940743d2 | ||
|
|
2e3c2cf592 | ||
|
|
42bc7f6cff | ||
|
|
96f4c115c9 | ||
|
|
327c322184 | ||
|
|
48b7109588 | ||
|
|
4f0c079e27 | ||
|
|
b0cfff9826 | ||
|
|
017bbcb901 | ||
|
|
9ecb340822 | ||
|
|
a0f41f95a0 | ||
|
|
b5efbe3f4c | ||
|
|
e6d191a83f | ||
|
|
c91bb6124a | ||
|
|
9688571a83 | ||
|
|
514ad0aa8e | ||
|
|
dc2efc6b9a | ||
|
|
1872d44b58 | ||
|
|
196b47757a | ||
|
|
a60a29549f | ||
|
|
2f3fed2fdb | ||
|
|
fe9dadfc7d | ||
|
|
41dcfec489 | ||
|
|
052e08e764 | ||
|
|
1f955af353 | ||
|
|
ccb027b515 | ||
|
|
f50b99b66c | ||
|
|
883a53fea0 | ||
|
|
b35a611538 | ||
|
|
1dca86f287 | ||
|
|
dd6cb410e6 | ||
|
|
27a9421e6a | ||
|
|
860782e6fb | ||
|
|
35232c3949 | ||
|
|
421da3f0d4 | ||
|
|
48970f61b4 | ||
|
|
f8984b8a93 | ||
|
|
6a8f921650 | ||
|
|
2cc459e24f | ||
|
|
475c0839c6 | ||
|
|
b886fb8edb | ||
|
|
8490a81867 | ||
|
|
652a7d74f9 | ||
|
|
52c123e6f4 | ||
|
|
43f88c99aa | ||
|
|
6073704fd7 | ||
|
|
6a69dd051a | ||
|
|
2840cdeced | ||
|
|
b1ce3c6d59 | ||
|
|
d359196adc | ||
|
|
cf19f30d26 | ||
|
|
aa3a3ea787 | ||
|
|
bd48b2b2d2 | ||
|
|
9a62841fd1 | ||
|
|
4c00df2e01 | ||
|
|
b5298fdde8 | ||
|
|
5a350108c4 | ||
|
|
1df5c39311 | ||
|
|
b3c7038cf4 | ||
|
|
8616d4e036 | ||
|
|
93c3da03eb | ||
|
|
68c2f5ae6d | ||
|
|
ff6b85e30b | ||
|
|
79c18ec836 | ||
|
|
790723e040 | ||
|
|
1ff2bd6d95 | ||
|
|
f592b336a3 | ||
|
|
37f35b12ea | ||
|
|
e4b34446af | ||
|
|
90ae81e9f8 | ||
|
|
9375e288ba | ||
|
|
06f70a6232 | ||
|
|
6906062187 | ||
|
|
dc4874a2f1 | ||
|
|
11608c51f0 | ||
|
|
8aa7fb8c2d | ||
|
|
00ae7c38e2 | ||
|
|
51a10e42f7 | ||
|
|
6655f4ec0e | ||
|
|
be64b553f9 | ||
|
|
66f07efd26 | ||
|
|
89e9976950 | ||
|
|
99d86e41ac | ||
|
|
ec334fda2c | ||
|
|
35d2ef0cbd | ||
|
|
512aa8bc93 | ||
|
|
f45126c8ec | ||
|
|
82d2552360 | ||
|
|
58d605083e | ||
|
|
0da18e4a5e | ||
|
|
f652d0ed38 | ||
|
|
6b40449c3f | ||
|
|
02088be2d1 | ||
|
|
14483c9254 | ||
|
|
fd7af1d462 | ||
|
|
8b95dc94f8 | ||
|
|
7626110d0b | ||
|
|
3c5ea297ff | ||
|
|
994048e6a4 | ||
|
|
c802c45e37 | ||
|
|
f4f8ff04db | ||
|
|
510b680fca | ||
|
|
afbe51abf8 | ||
|
|
52c2f4cb10 | ||
|
|
630639f747 | ||
|
|
11ffee5543 | ||
|
|
85810365a6 | ||
|
|
f0ebc183e1 | ||
|
|
ca2746f3bb | ||
|
|
b31b435091 | ||
|
|
1d126c833b | ||
|
|
9461196864 | ||
|
|
a2007db707 | ||
|
|
97bdde3135 | ||
|
|
090b2cc000 | ||
|
|
82b092ffc2 | ||
|
|
5979521309 | ||
|
|
1ca7b2a519 | ||
|
|
00f6f4e4a8 | ||
|
|
e2c35cf394 | ||
|
|
a3eb7ef302 | ||
|
|
2fc91663d2 | ||
|
|
fd80b08c90 | ||
|
|
453b796df0 | ||
|
|
e878ab710f | ||
|
|
13fcfc0f3b | ||
|
|
0416c9ede4 | ||
|
|
dc9216512d | ||
|
|
d0731be2f0 | ||
|
|
a2e1618700 | ||
|
|
31635849e0 | ||
|
|
d03274ad5f | ||
|
|
0c860e81e1 | ||
|
|
daf6cbb5d9 | ||
|
|
912e1c414c | ||
|
|
0a43efb9a4 | ||
|
|
f7f0a56f35 | ||
|
|
ae2735cdb6 | ||
|
|
6624947b62 | ||
|
|
afd069b169 | ||
|
|
6452aeffa1 | ||
|
|
4318a63bd8 | ||
|
|
ca08825935 | ||
|
|
c060aa0031 | ||
|
|
ecba49f711 | ||
|
|
17ce21c038 | ||
|
|
5e244e7382 | ||
|
|
b5b5f1f94b | ||
|
|
5b5f315b87 | ||
|
|
c4c2e66070 | ||
|
|
d8d5e1dd35 | ||
|
|
59a00631ed | ||
|
|
b2d6b954a1 | ||
|
|
1947add842 | ||
|
|
6b71d75bde | ||
|
|
f53252e292 | ||
|
|
61e7d31605 | ||
|
|
033047b7d4 | ||
|
|
d68a68fd0d | ||
|
|
31a6e74770 | ||
|
|
7c0a2a556c | ||
|
|
0dd71286f2 | ||
|
|
b350e079f8 | ||
|
|
24872fda7c | ||
|
|
5ad7cf4fb9 | ||
|
|
60b9bf7558 | ||
|
|
d6e8b44bcb | ||
|
|
fa3f526785 | ||
|
|
7d5615998c | ||
|
|
d7e4bba0e7 | ||
|
|
d3d0f653e5 | ||
|
|
9e0e146984 | ||
|
|
fc4465f78b | ||
|
|
0eaa7c3c84 | ||
|
|
5e4968492f | ||
|
|
a81547f244 | ||
|
|
bdb9a91db0 | ||
|
|
3b336b2eca | ||
|
|
9d7c636db9 | ||
|
|
c19204289d | ||
|
|
8f8358cc67 | ||
|
|
20a94fbb56 | ||
|
|
da25b18db7 | ||
|
|
02d9901b49 | ||
|
|
13be6ca27c | ||
|
|
e26abaef7d | ||
|
|
583398d186 | ||
|
|
5cc6ca6f5b | ||
|
|
b12a187d4f | ||
|
|
974a819ba1 | ||
|
|
d27fbb3b7d | ||
|
|
ea34a2356c | ||
|
|
fa9f104a93 | ||
|
|
51f73840a1 | ||
|
|
2b12de14ee | ||
|
|
7865051fdf | ||
|
|
4b0adb6863 | ||
|
|
ba5737fe14 | ||
|
|
7b13cb410d | ||
|
|
bb1f40e35f | ||
|
|
7be1628a74 | ||
|
|
3eb4bae9ab | ||
|
|
1f5cfe8973 | ||
|
|
5fc4a06af6 | ||
|
|
a1eaad6033 | ||
|
|
1af3269adb | ||
|
|
1ca54bcc25 | ||
|
|
7ba6efe805 | ||
|
|
4d667f1b08 | ||
|
|
5c38d372c1 | ||
|
|
9e4010695b | ||
|
|
4daea093f9 | ||
|
|
1d640c40e5 | ||
|
|
b0e32fe5be | ||
|
|
65f086d2d0 | ||
|
|
de6388aeca | ||
|
|
55108ee877 | ||
|
|
231e72fdde | ||
|
|
a079a6596a | ||
|
|
ed2360d7ea | ||
|
|
b717ac7f0b | ||
|
|
c619ff8474 | ||
|
|
2448a38b55 | ||
|
|
5fd3461142 | ||
|
|
c1319917cf | ||
|
|
a5015e1fc4 | ||
|
|
738ef1c73c | ||
|
|
5af29a3b93 | ||
|
|
62a8919a1c | ||
|
|
1ea00258d0 | ||
|
|
023b38354f | ||
|
|
a765b3c176 | ||
|
|
ca47fa352c | ||
|
|
680a992fc9 | ||
|
|
f2e55bf4d7 | ||
|
|
c96d36ec95 | ||
|
|
e4f97a53ec | ||
|
|
9e175bd491 | ||
|
|
e7d348868c | ||
|
|
c0b23ff791 | ||
|
|
322cc5e5f1 | ||
|
|
23dc073e65 | ||
|
|
a5d8db2445 | ||
|
|
de28eea11f | ||
|
|
66843cc95c | ||
|
|
f867e3e2ec | ||
|
|
1716e99151 | ||
|
|
483cd56924 | ||
|
|
b6c6f1add5 | ||
|
|
3442a6a53e | ||
|
|
9d9e90960e | ||
|
|
fcd07ba0af | ||
|
|
405727805a | ||
|
|
a46493724b | ||
|
|
d8eeb0989b | ||
|
|
a266e50e8e | ||
|
|
5295bd8227 | ||
|
|
7fbc59eb69 | ||
|
|
fa79b1ebc3 | ||
|
|
0c0df031c5 | ||
|
|
85ed17b313 | ||
|
|
b754ff6f1b | ||
|
|
e35123b8ac | ||
|
|
7f1f85cb76 | ||
|
|
935db68517 | ||
|
|
1c0a69c0ce | ||
|
|
27f4398b50 | ||
|
|
e47255715d | ||
|
|
d0ed0eacac | ||
|
|
ece1ecccca | ||
|
|
b13200d7a1 | ||
|
|
386f321d3c | ||
|
|
60a64568bf | ||
|
|
de98bdff1e | ||
|
|
992c4f4b89 | ||
|
|
d1ba57977d | ||
|
|
7c700729c2 | ||
|
|
87957a608b | ||
|
|
0d02000592 | ||
|
|
8d88b66668 | ||
|
|
763443abf2 | ||
|
|
7f18f3e9af | ||
|
|
b86849e442 | ||
|
|
319299519b | ||
|
|
23a1d38187 | ||
|
|
1582347204 | ||
|
|
d1556a7a96 | ||
|
|
d2416a9b68 | ||
|
|
fb58dfbd93 | ||
|
|
b8d52fad34 | ||
|
|
d2e319b2a1 | ||
|
|
0892ef3a9a | ||
|
|
dd292e3b00 | ||
|
|
95134201b2 | ||
|
|
acdf3c54da | ||
|
|
fc96132e72 | ||
|
|
8f6bbb59c7 | ||
|
|
fbb9006ade | ||
|
|
e009ea3036 | ||
|
|
63c079d873 | ||
|
|
e509937382 | ||
|
|
5090ffe05e | ||
|
|
bbdcfbac0a | ||
|
|
7403a01177 | ||
|
|
53b4bbe736 | ||
|
|
8bb48622a3 | ||
|
|
f28adb6ddb | ||
|
|
19794bc46a | ||
|
|
29eead917f | ||
|
|
fabdfb8a5b | ||
|
|
1411b7eb7e | ||
|
|
ef51690b5a | ||
|
|
1bfe0d3728 | ||
|
|
783a0a9830 | ||
|
|
cd2a4357c2 | ||
|
|
f3a2dcabba | ||
|
|
7bf3d0b585 | ||
|
|
cb2bb6084c | ||
|
|
00591cbe9a | ||
|
|
ba26e543f9 | ||
|
|
f6eeed3d5a | ||
|
|
3b0bb60ea3 | ||
|
|
26c09b7de6 | ||
|
|
4d5a4ab0e5 | ||
|
|
50f79e11fc | ||
|
|
6b09c74e35 | ||
|
|
b252821c59 | ||
|
|
68aa26477f | ||
|
|
7e530c9d4e | ||
|
|
1a9d49e16f | ||
|
|
aea725f2ea | ||
|
|
74431ffad8 | ||
|
|
0c9e83b59c | ||
|
|
09f88865e0 | ||
|
|
1b45d21328 | ||
|
|
d5cea617a5 | ||
|
|
4275047d16 | ||
|
|
4dbcdce43c | ||
|
|
6477143533 | ||
|
|
c63bd14a02 | ||
|
|
2d37088bd9 | ||
|
|
a8a1a3a958 | ||
|
|
4ccdead2a2 | ||
|
|
f7280213f7 | ||
|
|
db8692d679 | ||
|
|
37238c0985 | ||
|
|
7b282c4f1d | ||
|
|
d054ee9af3 | ||
|
|
9612a70c77 | ||
|
|
24f4736f64 | ||
|
|
9d5c8d961c | ||
|
|
7b4de8b6ae | ||
|
|
bcc73f8d40 | ||
|
|
4805f069e4 | ||
|
|
cfd2fc779b | ||
|
|
0920e06a42 | ||
|
|
3fdd54ca12 | ||
|
|
cf10d6d01d | ||
|
|
a9c7c6e8bf | ||
|
|
c12d0131cb | ||
|
|
0f4d8acaa8 | ||
|
|
94225b5ed3 | ||
|
|
a9ff663387 | ||
|
|
57b7dbdc85 | ||
|
|
f79b8bf96c | ||
|
|
e74c690ac4 | ||
|
|
efe9aab1e4 | ||
|
|
0952ce3410 | ||
|
|
d099c06945 | ||
|
|
aeab57b9b2 | ||
|
|
18328aed05 | ||
|
|
48a86ec926 | ||
|
|
b4e560824a | ||
|
|
9efc167482 | ||
|
|
5050471822 | ||
|
|
c84a3d50a3 | ||
|
|
cd1834e0a9 | ||
|
|
0932e8ce0a | ||
|
|
ffa43607e5 | ||
|
|
57e9f6a4b8 | ||
|
|
71c0eb5126 | ||
|
|
10fd1ada28 | ||
|
|
53fd5f8351 | ||
|
|
55bde0857f | ||
|
|
5f75e0d2a7 | ||
|
|
efd86c63e6 | ||
|
|
ea68c0e82c | ||
|
|
40e7ce7fbf | ||
|
|
d52ab27def | ||
|
|
7e4164c1cf | ||
|
|
44831bb854 | ||
|
|
131893e449 | ||
|
|
4daabc3d84 | ||
|
|
acc05620cb | ||
|
|
406be7c518 | ||
|
|
b51f0759d4 | ||
|
|
be313735dc | ||
|
|
017e9e98d1 | ||
|
|
611c30f04f | ||
|
|
be4a4990ff | ||
|
|
aba34524f1 | ||
|
|
881ab1b0c6 | ||
|
|
fe36f4a914 | ||
|
|
f8e5a0d94f | ||
|
|
0838e0e610 | ||
|
|
98321a054e | ||
|
|
ee9ac3e45f | ||
|
|
1a32b91eb2 | ||
|
|
18bfa4bf20 | ||
|
|
31f3003ff5 | ||
|
|
2f16e0984e | ||
|
|
a9ed218989 | ||
|
|
18d483c2a1 | ||
|
|
8e34ca5e31 | ||
|
|
130ba634ad | ||
|
|
6a4938bdfb | ||
|
|
14636d2f0e | ||
|
|
0ed735d3ba | ||
|
|
21532add46 | ||
|
|
d7ee1ab6be | ||
|
|
881f320269 | ||
|
|
da74bf13a4 | ||
|
|
86f93c28ee | ||
|
|
30ed40a56a | ||
|
|
b8275209e8 | ||
|
|
1e231ee2ae | ||
|
|
25c21aedda | ||
|
|
2d9af8592f | ||
|
|
8447bdd368 | ||
|
|
4f6f206458 | ||
|
|
6582260bc8 | ||
|
|
32682a4f84 | ||
|
|
55a95d4c67 | ||
|
|
a939553c87 | ||
|
|
badfad8b72 | ||
|
|
1be482c313 | ||
|
|
325213ae66 | ||
|
|
0a215c9318 | ||
|
|
2061508ad8 | ||
|
|
fb90414c05 | ||
|
|
9fade3e8c2 | ||
|
|
1920a7d687 | ||
|
|
a444551a6d | ||
|
|
0bba06d64a | ||
|
|
150c52b8de | ||
|
|
3e4b6a09b2 | ||
|
|
dd2e6b3680 | ||
|
|
d4439129a7 | ||
|
|
1e1d15cca5 | ||
|
|
f7e959ae5f | ||
|
|
008391f699 | ||
|
|
a4c517917b | ||
|
|
e9169901d2 | ||
|
|
0e1329b520 | ||
|
|
29bce46a2a | ||
|
|
13c1d4cde2 | ||
|
|
087987070b | ||
|
|
4bd7132ba9 | ||
|
|
28a4ea914d | ||
|
|
5ecfaa4837 | ||
|
|
06af9f871c | ||
|
|
7dace2fecb | ||
|
|
a985de062c | ||
|
|
7511e4de59 | ||
|
|
52558381c6 | ||
|
|
e3cce574d7 | ||
|
|
b18e4ce455 | ||
|
|
3d6c207723 | ||
|
|
a1d5968b24 | ||
|
|
e5272c130c | ||
|
|
196c15b5cd | ||
|
|
69d49744cf | ||
|
|
62635532a6 | ||
|
|
bc8da78150 | ||
|
|
5d781ee6b7 | ||
|
|
db76cc4e4e | ||
|
|
d2c2b46c9e | ||
|
|
7e152e4279 | ||
|
|
bdb42e22f8 | ||
|
|
9383e0b154 | ||
|
|
c7ed614240 | ||
|
|
a59f0c7fac | ||
|
|
0a378f3432 | ||
|
|
f2b51e3b24 | ||
|
|
329641a86f | ||
|
|
f0dd497780 | ||
|
|
99b93b2985 | ||
|
|
67845665d6 | ||
|
|
3a070ec1bf | ||
|
|
749cd7c354 | ||
|
|
eaa4083e23 | ||
|
|
3873215908 | ||
|
|
2a4f3d1a71 | ||
|
|
4ff3e6dfb9 | ||
|
|
f78c4b241e | ||
|
|
e67ffda7ae | ||
|
|
d2067668d3 | ||
|
|
ba57ea914b | ||
|
|
5948988aae | ||
|
|
76b3f3c7cf | ||
|
|
db15ddf07f | ||
|
|
5d5b395d00 | ||
|
|
cf3aca6e3b | ||
|
|
956f4672e4 | ||
|
|
6206ce940e | ||
|
|
5968fd0ed9 | ||
|
|
c6727ed4c9 | ||
|
|
c24da29f6b | ||
|
|
baa39f39e8 | ||
|
|
9fedf899e2 | ||
|
|
8e23e13158 | ||
|
|
bb0b50d8bc | ||
|
|
429d6f0480 | ||
|
|
5bafffed51 | ||
|
|
781a02aea7 | ||
|
|
3645ce5f43 | ||
|
|
7cf90c79d6 | ||
|
|
195a331322 | ||
|
|
29371b0819 | ||
|
|
f744375a4c | ||
|
|
8e7954438d | ||
|
|
a65d476f25 | ||
|
|
961d9e94f3 | ||
|
|
51c5d03888 | ||
|
|
9b6a49b8e7 | ||
|
|
213de0797e | ||
|
|
d8326fccfc | ||
|
|
9eb120dd40 | ||
|
|
4726bdd08b | ||
|
|
8ea4dc56f0 | ||
|
|
23eef9ecc3 | ||
|
|
cdb5cfaf6c | ||
|
|
dfa83fbe93 | ||
|
|
2dff7af0f9 | ||
|
|
1067e89fab | ||
|
|
04c45e9dc2 | ||
|
|
5ee436d8a2 | ||
|
|
e5e04de137 | ||
|
|
fa565eafb1 | ||
|
|
eacff0a629 | ||
|
|
c0d5119375 | ||
|
|
22dffa1bb9 | ||
|
|
efcc2252e5 | ||
|
|
981279c996 | ||
|
|
c3b3072a6f | ||
|
|
489968fd7a | ||
|
|
7c008bd8c1 | ||
|
|
9c30951e4a | ||
|
|
40cf3f50a6 | ||
|
|
75b7dd1e0b | ||
|
|
642d20b8ac | ||
|
|
623c32e391 | ||
|
|
310f8b7fa2 | ||
|
|
6254aba5e9 | ||
|
|
7145484fde | ||
|
|
fd300ab6f3 | ||
|
|
88011a6a2e | ||
|
|
14029e50aa | ||
|
|
3fb03cdac1 | ||
|
|
8f9a9a255b | ||
|
|
bae37a00fb | ||
|
|
4543ddc5c1 | ||
|
|
5af7e7438a | ||
|
|
613722eee4 | ||
|
|
66a10d4939 | ||
|
|
d0ca057ca4 | ||
|
|
e4c369bd56 | ||
|
|
1035a4daf0 | ||
|
|
242907854d | ||
|
|
0116c5a1e1 | ||
|
|
67d3a896bd | ||
|
|
501cc19d0e | ||
|
|
8efe2fd8cf | ||
|
|
0aac71c6ee | ||
|
|
f4dd88b001 | ||
|
|
0be30886a5 | ||
|
|
324a47e2b7 | ||
|
|
2493b0dc31 | ||
|
|
264cbbec26 | ||
|
|
87e01596b7 | ||
|
|
43f9383d40 | ||
|
|
685e97a3a6 | ||
|
|
e71d291b39 | ||
|
|
4adcef4ff4 | ||
|
|
799e9938fb | ||
|
|
8f2cba0c20 | ||
|
|
6feefb7aec | ||
|
|
9d475f9edb | ||
|
|
ab9bd9e877 | ||
|
|
b112805ffa | ||
|
|
8c56eabcdc | ||
|
|
d3bd614ba7 | ||
|
|
e9f5add97c | ||
|
|
ab3b398b5f | ||
|
|
5032d06697 | ||
|
|
bed1e3c362 | ||
|
|
a5b07b2960 | ||
|
|
df8eceba0c | ||
|
|
95a04c8dff | ||
|
|
cd1ee1d1ad | ||
|
|
9e7c2d713c | ||
|
|
06b61ba96f | ||
|
|
4a4b50d807 | ||
|
|
1c0932e173 | ||
|
|
a4edda36d5 | ||
|
|
1ed36679bd | ||
|
|
4e5632c3e3 | ||
|
|
44bc47a3e3 | ||
|
|
d4e8ddb0c5 | ||
|
|
21323c31f8 | ||
|
|
c76db386c0 | ||
|
|
58e6e6b325 | ||
|
|
567ea21dc8 | ||
|
|
b9d0728687 | ||
|
|
de76baaa0d | ||
|
|
26798773cb | ||
|
|
98d86915be | ||
|
|
5de5e2bed0 | ||
|
|
a0babc93dd | ||
|
|
7ccc8a6f50 | ||
|
|
29a36a10be | ||
|
|
f180fbb884 | ||
|
|
1d4e0436e4 | ||
|
|
270eac6973 | ||
|
|
266ac9c11b | ||
|
|
ac770d98fb | ||
|
|
fd880839f6 | ||
|
|
570106ac17 | ||
|
|
775348b402 | ||
|
|
d9efbf8229 | ||
|
|
233f3dffe6 | ||
|
|
f9d7826711 | ||
|
|
c54897a59d | ||
|
|
29646ba6b4 | ||
|
|
6f0d0ababc | ||
|
|
52f7bcaedd | ||
|
|
95acbf1f03 | ||
|
|
43569bb44c | ||
|
|
15557e7796 | ||
|
|
e2fd418a96 | ||
|
|
4e9b01fd59 | ||
|
|
aa167a25a7 | ||
|
|
5cbb9191a7 | ||
|
|
ae4cc14b28 | ||
|
|
fa556700c2 | ||
|
|
68773aab98 | ||
|
|
1e6114c270 | ||
|
|
9e7039ebca | ||
|
|
1923d1c27d | ||
|
|
b638445234 | ||
|
|
9bcf8a5b80 | ||
|
|
979fe247f9 | ||
|
|
6378fb0cba | ||
|
|
3ee92b40c5 | ||
|
|
4892b8f17e | ||
|
|
28b807a735 | ||
|
|
4e5d67df35 | ||
|
|
dbe92a1c24 | ||
|
|
188e3a9f90 | ||
|
|
10940ecf39 | ||
|
|
3495635a52 | ||
|
|
c8deeb0b05 | ||
|
|
b6ee9f0a32 | ||
|
|
6c8ad0bd4a | ||
|
|
7137fae2a6 | ||
|
|
859de4c8fd | ||
|
|
e835f79819 | ||
|
|
99eba19ffc | ||
|
|
38cfd7e2d9 | ||
|
|
3fc08745a6 | ||
|
|
efc6e4a816 | ||
|
|
df64b5f368 | ||
|
|
772a0382f0 | ||
|
|
8fa67d477d | ||
|
|
ef891643e9 | ||
|
|
4a7739acdf | ||
|
|
14e0a49e17 | ||
|
|
b439dddfda | ||
|
|
7e62731863 | ||
|
|
b42157a725 | ||
|
|
335fcdc1d6 | ||
|
|
e094a3f40b | ||
|
|
57387445f3 | ||
|
|
2fc24d8879 | ||
|
|
cc91155fe4 | ||
|
|
b7dba62678 | ||
|
|
5485f368b2 | ||
|
|
f8351e9e33 | ||
|
|
140e172951 | ||
|
|
941a0487ab | ||
|
|
1d4b9ead45 | ||
|
|
7f0f579bb0 | ||
|
|
dfb3978b9d | ||
|
|
2031c1d5ad | ||
|
|
6f2295bfc7 | ||
|
|
8193b80a59 | ||
|
|
f6d580200a | ||
|
|
b491c3271b | ||
|
|
d106b3b5a0 | ||
|
|
10fe591e54 | ||
|
|
66cd8a72bc | ||
|
|
dc46daa690 | ||
|
|
65246e69d7 | ||
|
|
de69bf1e03 | ||
|
|
eba85dc478 | ||
|
|
abf70750fd | ||
|
|
88b697d5df | ||
|
|
3eb5a84ea1 | ||
|
|
71e5a4baf7 | ||
|
|
4bfcbd0c78 | ||
|
|
2b1e5e7f5c | ||
|
|
cbb4b7e53e | ||
|
|
81c0f11d7a | ||
|
|
561fc1c3e4 | ||
|
|
43491f67a1 | ||
|
|
c05bc4d3cc | ||
|
|
b9ebc8f209 | ||
|
|
f7c48ba59a | ||
|
|
22c16d0517 | ||
|
|
2cc1e7ef90 | ||
|
|
35e69af41b | ||
|
|
81ff9384cb | ||
|
|
0d3dbae710 | ||
|
|
19382f275e | ||
|
|
0aaed397cc | ||
|
|
909b8c7ef4 | ||
|
|
b8ff74ad70 | ||
|
|
96cf8bd76a | ||
|
|
1f716aa181 | ||
|
|
111c41cf3f | ||
|
|
787a6a33df | ||
|
|
f8e1e7fed6 | ||
|
|
f09a5b602e | ||
|
|
027e3f2d54 | ||
|
|
8116d235ef | ||
|
|
959cbe3d87 | ||
|
|
c0420e2b8a | ||
|
|
9578ddbddd | ||
|
|
e274de45b7 | ||
|
|
ce328fb37a | ||
|
|
be4ab92b2b | ||
|
|
8f7f2b5d92 | ||
|
|
3efeba33a4 | ||
|
|
ce157b32ed | ||
|
|
3cb2381bdc | ||
|
|
a7d2c782df | ||
|
|
a3aa9bae6d | ||
|
|
c0c4ce1ce6 | ||
|
|
b939fa9d4a | ||
|
|
d03c42d798 | ||
|
|
6aabbdf547 | ||
|
|
ad4b2f30f5 | ||
|
|
ff758f37d2 | ||
|
|
2e31d8b702 | ||
|
|
8c8fd6e298 | ||
|
|
f62ec2e4a0 | ||
|
|
415a0dc9d0 | ||
|
|
b59f3bf46c | ||
|
|
2db78bd3d6 | ||
|
|
f88d62ad28 | ||
|
|
8363d537e0 | ||
|
|
b8ed245085 | ||
|
|
76efc52fb6 | ||
|
|
dc6b318c34 | ||
|
|
96884dd1ae | ||
|
|
0b061c5fa1 | ||
|
|
958a21ad6e | ||
|
|
6c2d2ed043 | ||
|
|
58f461b087 | ||
|
|
28167bbf85 | ||
|
|
985428d9d2 | ||
|
|
4a4fd35e0c | ||
|
|
f46038820b | ||
|
|
05ad6b49a1 | ||
|
|
a3675552bd | ||
|
|
b3ad9b67cf | ||
|
|
f49a393566 | ||
|
|
8a46947865 | ||
|
|
7ae0312091 | ||
|
|
58e35a4f32 | ||
|
|
95023e7d49 | ||
|
|
7a70de401c | ||
|
|
666a5c6f87 | ||
|
|
c7343d2aa2 | ||
|
|
a6430bfe20 | ||
|
|
32bf3d0f33 | ||
|
|
b97a798ebc | ||
|
|
cb31033972 | ||
|
|
3aeab296d8 | ||
|
|
f3bbb95310 | ||
|
|
00eabe8ab9 | ||
|
|
017cc62e41 | ||
|
|
a16070ccf6 | ||
|
|
ab99d56e96 | ||
|
|
b1ce821c49 | ||
|
|
578785ee7f | ||
|
|
ed7c8f2208 | ||
|
|
2e1c7c00a6 | ||
|
|
994ca26e92 | ||
|
|
d2f87c612e | ||
|
|
62cd63bfda | ||
|
|
dee35a841d | ||
|
|
fb44e2dd09 | ||
|
|
c42d78e26c | ||
|
|
2dc287f2e0 | ||
|
|
83094abf21 | ||
|
|
aafe93b335 | ||
|
|
cc52c93ad7 | ||
|
|
d7f048a265 | ||
|
|
55c5eed9fc | ||
|
|
0d9e7e1530 | ||
|
|
9b4fcfb897 | ||
|
|
cff3f89850 | ||
|
|
9517f12cdd | ||
|
|
40f4d450be | ||
|
|
620eb3fd00 | ||
|
|
c6ff69fd12 | ||
|
|
7b271bbee9 | ||
|
|
68f099707e | ||
|
|
3bdb869fca | ||
|
|
c227863d51 | ||
|
|
3a8c9f16de | ||
|
|
2da31290a1 | ||
|
|
3c53ba01a7 | ||
|
|
07bf8b92fd | ||
|
|
63dc24b5e8 | ||
|
|
8f1d6af3ed | ||
|
|
85f431a072 | ||
|
|
d7fb9b5d35 | ||
|
|
eba6ea5639 | ||
|
|
2ad6e8fb75 | ||
|
|
570bd084c2 | ||
|
|
15278fe2f9 | ||
|
|
c4e6dcfd80 | ||
|
|
701f844755 | ||
|
|
91cf660377 | ||
|
|
78e1aaedb9 | ||
|
|
708b85a6a4 | ||
|
|
1cab4d8c22 | ||
|
|
d8b9a83f8c | ||
|
|
a016bd4ba5 | ||
|
|
983a3db3af | ||
|
|
7969751f1f | ||
|
|
6271c517da | ||
|
|
4aaf5cb73b | ||
|
|
e9bad40b10 | ||
|
|
1e28ef5942 | ||
|
|
28f673e4cd | ||
|
|
9ddf69698d | ||
|
|
51631d0324 | ||
|
|
c7538cd256 | ||
|
|
8ed57b000c | ||
|
|
27bcec7645 | ||
|
|
6cf645f87c | ||
|
|
aa556c372f | ||
|
|
126ac24c8c | ||
|
|
3c8be00131 | ||
|
|
f5ae32e53f | ||
|
|
a3a6a85b73 | ||
|
|
db15e0090a | ||
|
|
994192ef4f | ||
|
|
3e60c3b86b | ||
|
|
de27905e88 | ||
|
|
28dde982cb | ||
|
|
de8d756afa | ||
|
|
b1748c4ea9 | ||
|
|
432ebc498c | ||
|
|
abc07bb079 | ||
|
|
78fc7b171e | ||
|
|
4c1f920d58 | ||
|
|
e9e3c46cb7 | ||
|
|
62198286e9 | ||
|
|
af90a9bd1f | ||
|
|
5e4958c889 | ||
|
|
fdc2ec17e6 | ||
|
|
b6740c937f | ||
|
|
10c8f523c4 | ||
|
|
8249ab37db | ||
|
|
1859519573 | ||
|
|
9be9c0b68c | ||
|
|
8ed48e27a6 | ||
|
|
a7f1ed8fa4 | ||
|
|
f5e4da5e72 | ||
|
|
9dc4c40bff | ||
|
|
81fd853005 | ||
|
|
01bd9b71a2 | ||
|
|
9ad3865690 | ||
|
|
6f9fde093f | ||
|
|
5097b31457 | ||
|
|
aa23648835 | ||
|
|
a774230661 | ||
|
|
222181efd6 | ||
|
|
1a33dc8cc4 | ||
|
|
3510aa22b5 | ||
|
|
77729e2723 | ||
|
|
05ea883e26 | ||
|
|
6566039bc8 | ||
|
|
e50bffb7e3 | ||
|
|
f2804eb2c8 | ||
|
|
559960865a | ||
|
|
a4fb58e428 | ||
|
|
a668aa8b36 | ||
|
|
8d0b5ede28 | ||
|
|
2786ae9de6 | ||
|
|
34b8bf1a6e | ||
|
|
277aa1411d | ||
|
|
52c7d88340 | ||
|
|
9e2d97f250 | ||
|
|
bfe0a5a873 | ||
|
|
504919a054 | ||
|
|
0d27f45ead | ||
|
|
65380be9c8 | ||
|
|
d596d0844f | ||
|
|
cd04848b97 | ||
|
|
e377c4358c | ||
|
|
3438580213 | ||
|
|
e93e6368fc | ||
|
|
200b1406d7 | ||
|
|
421a83282a | ||
|
|
a2fb09260a | ||
|
|
aa7a811e66 | ||
|
|
2ac784c51a | ||
|
|
6b39d0b453 | ||
|
|
f218c8d40a | ||
|
|
1cce2b38ee | ||
|
|
9d9ed1cc62 | ||
|
|
2d7767eaf2 | ||
|
|
064d9b74e8 | ||
|
|
e87c47c957 | ||
|
|
bdedd6c544 | ||
|
|
5fb5dfb34c | ||
|
|
9908d3ca2f | ||
|
|
4df6b0636b | ||
|
|
498e06c582 | ||
|
|
e6722126ba | ||
|
|
e619ca386e | ||
|
|
8224434d21 | ||
|
|
fd55f3248f | ||
|
|
08d6c153b7 | ||
|
|
636d6be690 | ||
|
|
7dcf728461 | ||
|
|
fb409787c6 | ||
|
|
e7caedc22a | ||
|
|
eebf4e3ffb | ||
|
|
c7cf84ad39 | ||
|
|
0681aa2566 | ||
|
|
17747df842 | ||
|
|
3fbce499d8 | ||
|
|
1a40e5690f | ||
|
|
7903716ad1 | ||
|
|
f8eae45048 | ||
|
|
a44a1019f8 | ||
|
|
9fba8ed43b | ||
|
|
7f5e915dd1 | ||
|
|
7e06816571 | ||
|
|
e9e66da5a8 | ||
|
|
94bb9354ce | ||
|
|
95982a508f | ||
|
|
563979e91e | ||
|
|
1cb28fd429 | ||
|
|
aa679efe41 | ||
|
|
e2cfaa46e7 | ||
|
|
593fc93659 | ||
|
|
530fb20f25 | ||
|
|
200602b41b | ||
|
|
e2624fb65c | ||
|
|
cc6a8e5174 | ||
|
|
a10ff1255c | ||
|
|
6d5f027848 | ||
|
|
e21b849a92 | ||
|
|
ba345f1052 | ||
|
|
c74a4f521c | ||
|
|
7c01ec9be8 | ||
|
|
163eb7ffae | ||
|
|
52374af940 | ||
|
|
b6a7b8b7df | ||
|
|
1dc0a09009 | ||
|
|
e98ef32eb2 | ||
|
|
863667ce67 | ||
|
|
8f5451587e | ||
|
|
85b593fd2f | ||
|
|
0fcec63b68 | ||
|
|
8553c91c1a | ||
|
|
c66e700f05 | ||
|
|
fdf9751c12 | ||
|
|
1be69e56aa | ||
|
|
e7c029875c | ||
|
|
0678b24d58 | ||
|
|
b369fbca15 | ||
|
|
36ee193e3c | ||
|
|
1dd0f2e5dc | ||
|
|
f34735f462 | ||
|
|
a2d7d41185 | ||
|
|
03ab687686 | ||
|
|
2e756b71f2 | ||
|
|
fbc69322af | ||
|
|
414489d152 | ||
|
|
50a9dd7a64 | ||
|
|
a4156eea10 | ||
|
|
e0ecb55d4b | ||
|
|
897faea333 | ||
|
|
0e08c39999 | ||
|
|
ca9b4d73f2 | ||
|
|
94edcd5d81 | ||
|
|
3673702af5 | ||
|
|
5778739cdb | ||
|
|
81e461f651 | ||
|
|
b7aca87fd0 | ||
|
|
ae1bcdc25d | ||
|
|
263605779c | ||
|
|
9eabd2a5b7 | ||
|
|
d66bc88e68 | ||
|
|
8177f27aa3 | ||
|
|
d5a0124637 | ||
|
|
30b29e51e8 | ||
|
|
9f1effd71c | ||
|
|
6532d5b61e | ||
|
|
57c76584a6 | ||
|
|
82105f260a | ||
|
|
50ad6e2e50 | ||
|
|
f323d84644 | ||
|
|
3a9070359b | ||
|
|
d39fc491ec | ||
|
|
2b8cb628b6 | ||
|
|
8b4b3f4a94 | ||
|
|
624e4eef64 | ||
|
|
a3bbf33c05 | ||
|
|
7c616db07a | ||
|
|
5fea83dcae | ||
|
|
0aef702b66 | ||
|
|
b5827cfd54 | ||
|
|
ea4291a92c | ||
|
|
32ac921f24 | ||
|
|
ed12ea7163 | ||
|
|
eb4237a2b0 | ||
|
|
706b5676a6 | ||
|
|
c31fe2ed9e | ||
|
|
987588eaa1 | ||
|
|
804679d996 | ||
|
|
571d1e1d9b | ||
|
|
3c22e78af0 | ||
|
|
5d9c7abed5 | ||
|
|
ac18c588c8 | ||
|
|
b83284071a | ||
|
|
fd13c72e95 | ||
|
|
3c2e9a43b9 | ||
|
|
ca319d09f2 | ||
|
|
46598dd491 | ||
|
|
3045b3ade1 | ||
|
|
604360d1cc | ||
|
|
25dc5742ea | ||
|
|
1136e290fd | ||
|
|
6506c0ea68 | ||
|
|
1323d1645d | ||
|
|
f1d845c630 | ||
|
|
5688bcba91 | ||
|
|
1a61eb7151 | ||
|
|
1cc6999ada | ||
|
|
88cb79d8b3 | ||
|
|
32ad1dcdce | ||
|
|
c037766040 | ||
|
|
da10822d7d | ||
|
|
5345881b1d | ||
|
|
c5ff640dff | ||
|
|
02eee39ddf | ||
|
|
e652f6e20a | ||
|
|
c4e46c3be8 | ||
|
|
0756c98102 | ||
|
|
7b693ae429 | ||
|
|
d4754c211a | ||
|
|
423f895755 | ||
|
|
c8a6e93f33 | ||
|
|
446042e518 | ||
|
|
1e6926cb9c | ||
|
|
b4b5371640 | ||
|
|
6d339ec93b | ||
|
|
e866ff6ec4 | ||
|
|
c83dced4ba | ||
|
|
d674a04f2a | ||
|
|
9b61c1bdd9 | ||
|
|
add86cc3bc | ||
|
|
dc332d141c | ||
|
|
59bb131df4 | ||
|
|
443537daae | ||
|
|
b588c0bc42 | ||
|
|
4e9c1d7c8b | ||
|
|
a0b313018a | ||
|
|
fafc4d288d | ||
|
|
de084cdfc2 | ||
|
|
7eed2d3b7d | ||
|
|
f7dca22ea5 | ||
|
|
5eb8628ff2 | ||
|
|
002aaaa3d0 | ||
|
|
0f6ad105fe | ||
|
|
28fa458dc7 | ||
|
|
eca8d6b9ab | ||
|
|
81ac8ccd64 | ||
|
|
d24de611f8 | ||
|
|
f83fe6435b | ||
|
|
ee2c8b97bb | ||
|
|
d36f4dd6c6 | ||
|
|
1e8745c887 | ||
|
|
96e9ab8662 | ||
|
|
0eddfe1cfa | ||
|
|
acd2f09ddd | ||
|
|
5929cdd38c | ||
|
|
60756c0ca4 | ||
|
|
b56c61eccd | ||
|
|
f4e3eb1c5a | ||
|
|
fd872550d5 | ||
|
|
f4820c00dc | ||
|
|
de6ef7134d | ||
|
|
e6ea7b9af0 | ||
|
|
9e7aff59df | ||
|
|
00efee4303 | ||
|
|
00b7c855cc | ||
|
|
4a02e54171 | ||
|
|
9192426d2e | ||
|
|
d077002eba | ||
|
|
5f09442fce | ||
|
|
44003d7f6b | ||
|
|
e1eff57c25 | ||
|
|
d881bfc0de | ||
|
|
6ff771d3d3 | ||
|
|
efb46db0a9 | ||
|
|
8ba81615e0 | ||
|
|
50ba146298 | ||
|
|
b9083fc7ad | ||
|
|
1bf76d26d7 | ||
|
|
2abce65031 | ||
|
|
1ee929e4b9 | ||
|
|
6ca1e15b0a | ||
|
|
195b219707 | ||
|
|
66d664ff08 | ||
|
|
4795650d21 | ||
|
|
d522aed65e | ||
|
|
7634e3c23b | ||
|
|
d8694f03bc | ||
|
|
77b6c5bb0c | ||
|
|
5934981e22 | ||
|
|
db01fb98f8 | ||
|
|
32064d39e7 | ||
|
|
92a550973a | ||
|
|
a8cfe746b3 | ||
|
|
5705f4a4aa | ||
|
|
11ada80d58 | ||
|
|
3f1581b2d3 | ||
|
|
b7ec3cdadf | ||
|
|
fc589bce17 | ||
|
|
e39fce270d | ||
|
|
5a2f542ab0 | ||
|
|
b16ed03cad | ||
|
|
3e98a5a1ad | ||
|
|
e471be7302 | ||
|
|
22e1b8144a | ||
|
|
b1737cc841 | ||
|
|
3c5553e253 | ||
|
|
901a77cfb1 | ||
|
|
89ba561a6f |
@@ -0,0 +1,107 @@
|
||||
# Security Policy
|
||||
|
||||
[English](#english) | [中文](#中文)
|
||||
|
||||
## English
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues, pull requests, or discussions.**
|
||||
|
||||
Report them privately through GitHub's private vulnerability reporting:
|
||||
|
||||
**[Report a vulnerability](https://github.com/Wei-Shaw/sub2api/security/advisories/new)**
|
||||
|
||||
(Repository → **Security** tab → **Report a vulnerability**.) Only the maintainers can see the report, and the advisory becomes the place to discuss, fix, and eventually publish the issue.
|
||||
|
||||
If you cannot use GitHub private vulnerability reporting, email **dev@sub2api.org** instead, with `[Security]` in the subject line.
|
||||
|
||||
A useful report includes:
|
||||
|
||||
- The affected component and version (release tag or commit)
|
||||
- Impact: what an attacker can do and which privileges they need
|
||||
- Minimal reproduction steps or a proof of concept
|
||||
- Any non-default configuration required to trigger the issue
|
||||
- A suggested fix, if you have one
|
||||
|
||||
### Supported Versions
|
||||
|
||||
Sub2API releases frequently, and fixes ship in a new release rather than being backported.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| Latest release | ✅ |
|
||||
| Older releases | ❌ |
|
||||
|
||||
Please confirm that the issue reproduces on the latest release or on `main` before reporting.
|
||||
|
||||
### Scope
|
||||
|
||||
In scope: the Sub2API backend, frontend, official Docker images, and deployment files in this repository.
|
||||
|
||||
Usually out of scope:
|
||||
|
||||
- Issues that require already-compromised admin credentials or server access
|
||||
- Risks arising from explicitly weakening documented security settings (for example, disabling `security.url_allowlist`)
|
||||
- Vulnerabilities in upstream AI providers or third-party dependencies with no Sub2API-specific impact (please report those upstream)
|
||||
- Missing best-practice headers or scanner output without a demonstrated impact
|
||||
|
||||
### Disclosure Process
|
||||
|
||||
1. We acknowledge the report and assess its validity and severity.
|
||||
2. We develop a fix privately in the advisory and may ask you to verify it.
|
||||
3. We publish a release containing the fix, then publish the GitHub Security Advisory (with a CVE when applicable), crediting you unless you prefer to stay anonymous.
|
||||
|
||||
Please keep details private until the advisory is published. This is a volunteer-maintained project, so response times are best effort.
|
||||
|
||||
## 中文
|
||||
|
||||
### 报告漏洞
|
||||
|
||||
**请不要通过公开的 Issue、Pull Request 或 Discussion 报告安全漏洞。**
|
||||
|
||||
请使用 GitHub 私密漏洞报告功能提交:
|
||||
|
||||
**[提交漏洞报告](https://github.com/Wei-Shaw/sub2api/security/advisories/new)**
|
||||
|
||||
(仓库 → **Security** 标签页 → **Report a vulnerability**。)报告仅维护者可见,后续的讨论、修复与公告发布都在该安全公告中进行。
|
||||
|
||||
如果无法使用 GitHub 私密漏洞报告,也可以发送邮件至 **dev@sub2api.org**,邮件标题请注明 `[Security]`。
|
||||
|
||||
报告中建议包含:
|
||||
|
||||
- 受影响的组件与版本(Release tag 或 commit)
|
||||
- 影响:攻击者能做什么、需要什么权限
|
||||
- 最小复现步骤或 PoC
|
||||
- 触发问题所需的非默认配置
|
||||
- 修复建议(如有)
|
||||
|
||||
### 支持的版本
|
||||
|
||||
Sub2API 发版频繁,安全修复随新版本发布,不向旧版本回移。
|
||||
|
||||
| 版本 | 是否支持 |
|
||||
| ---- | -------- |
|
||||
| 最新 Release | ✅ |
|
||||
| 更早的版本 | ❌ |
|
||||
|
||||
报告前请先确认问题在最新 Release 或 `main` 上仍可复现。
|
||||
|
||||
### 范围
|
||||
|
||||
范围内:本仓库中的 Sub2API 后端、前端、官方 Docker 镜像与部署文件。
|
||||
|
||||
通常不在范围内:
|
||||
|
||||
- 需要事先已掌握管理员凭据或服务器权限才能利用的问题
|
||||
- 因主动关闭文档中的安全配置(如关闭 `security.url_allowlist`)而产生的风险
|
||||
- 上游 AI 服务商或第三方依赖自身的漏洞,且对 Sub2API 无特定影响(请向上游报告)
|
||||
- 仅缺少最佳实践响应头或扫描器输出,但未证明实际影响
|
||||
|
||||
### 披露流程
|
||||
|
||||
1. 我们确认收到报告,并评估其有效性与严重程度。
|
||||
2. 我们在安全公告中私下开发修复,可能请你协助验证。
|
||||
3. 我们先发布包含修复的版本,再公开 GitHub 安全公告(适用时申请 CVE),并致谢报告者(如你希望匿名则不署名)。
|
||||
|
||||
在公告发布前请勿公开漏洞细节。本项目由志愿者维护,响应时间尽力而为。
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -0,0 +1,30 @@
|
||||
# Release matrix
|
||||
|
||||
The release workflow builds the frontend once, then runs each configured Go target on its own Linux runner. `CGO_ENABLED=0` permits cross-compilation. The target matrix is read from `.goreleaser.yaml`, including its exclusions; simple releases select Linux amd64 only.
|
||||
|
||||
Each build uses GoReleaser OSS in snapshot mode with the selected release version and one target. Archive naming, bundled files, Go flags and release templates remain in the existing GoReleaser configurations. Every archive is accompanied by its source commit, target, version and SHA256. The publishing job verifies the complete matrix before building images or publishing. It uses GoReleaser's `extra_files` support to publish existing archives and checksums, with builds disabled. No Pro license is needed.
|
||||
|
||||
Go caches are isolated by target and refreshed on each source commit, with fallback to the preceding target cache. Save uses the original restore key, even if a build hook changes `go.sum`. Matrix jobs upload uniquely named artifacts. The publishing job extracts only the regular Linux binary from each verified archive and restores its executable permission before constructing Docker contexts. QEMU remains limited to runtime-image instructions. DockerHub images are omitted when its credentials are absent; GHCR is always retained. Simple mode still publishes only the amd64 GHCR image and the simple release description.
|
||||
|
||||
All build jobs use the commit resolved by `prepare`, including a manual release's selected tag. Helper scripts come from the workflow revision and are passed as a run-local artifact, so older application tags do not need to contain the new scripts. The workflow serializes release runs to prevent simultaneous updates to moving image tags.
|
||||
|
||||
## Validate without publication
|
||||
|
||||
From a branch containing this workflow:
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml --ref <branch> \
|
||||
-f tag=<branch> -f dry_run=true -f simple_release=false
|
||||
```
|
||||
|
||||
A dry run builds all selected archives and both runtime images, verifies artifact provenance and produces the final checksum file. It exports images locally as OCI archives instead of pushing them. It skips registry logins, GitHub Release publication, DockerHub description updates, Telegram notifications and VERSION synchronization. Test the simple path separately with `simple_release=true`.
|
||||
|
||||
Dry-run artifacts are available in the Actions run, including `release-dry-run-report`. Compare job start/end times, GoReleaser's build duration and cache restore results. Do not present an initial cold-cache run as a warmed-cache benchmark; publishing network time is not measured by dry runs.
|
||||
|
||||
Helper checks:
|
||||
|
||||
```bash
|
||||
python -m pip install -r .github/release-tools/requirements-release.txt
|
||||
python -m unittest discover -s .github/release-tools -p 'test_release_matrix.py'
|
||||
bash -n .github/release-tools/release-images.sh
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${RELEASE_VERSION:?}" "${RELEASE_SHA:?}" "${GITHUB_REPOSITORY:?}" "${RUNNER_TEMP:?}"
|
||||
owner=${GITHUB_REPOSITORY%%/*}
|
||||
registries=("ghcr.io/${owner,,}/sub2api")
|
||||
if [[ ${SIMPLE_RELEASE:-false} != true && ${DOCKERHUB_USERNAME:-skip} != skip ]]; then
|
||||
registries+=("${DOCKERHUB_USERNAME}/sub2api")
|
||||
fi
|
||||
arches=(amd64 arm64)
|
||||
if [[ ${SIMPLE_RELEASE:-false} == true ]]; then arches=(amd64); fi
|
||||
for arch in "${arches[@]}"; do
|
||||
args=(--platform "linux/$arch" --file ".release-context/$arch/Dockerfile"
|
||||
--label "org.opencontainers.image.version=$RELEASE_VERSION"
|
||||
--label "org.opencontainers.image.revision=$RELEASE_SHA"
|
||||
--label "org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY")
|
||||
for registry in "${registries[@]}"; do
|
||||
args+=(--tag "$registry:$RELEASE_VERSION-$arch")
|
||||
if [[ ${SIMPLE_RELEASE:-false} == true ]]; then
|
||||
args+=(--tag "$registry:$RELEASE_VERSION" --tag "$registry:latest")
|
||||
fi
|
||||
done
|
||||
if [[ ${DRY_RUN:-false} == true ]]; then
|
||||
args+=(--output "type=oci,dest=$RUNNER_TEMP/sub2api-$arch.oci.tar")
|
||||
else
|
||||
args+=(--push)
|
||||
fi
|
||||
docker buildx build "${args[@]}" ".release-context/$arch"
|
||||
done
|
||||
if [[ ${DRY_RUN:-false} != true && ${SIMPLE_RELEASE:-false} != true ]]; then
|
||||
major=${RELEASE_VERSION%%.*}
|
||||
minor=${RELEASE_VERSION#*.}; minor=${minor%%.*}
|
||||
for registry in "${registries[@]}"; do
|
||||
docker buildx imagetools create \
|
||||
--tag "$registry:$RELEASE_VERSION" --tag "$registry:latest" \
|
||||
--tag "$registry:$major.$minor" --tag "$registry:$major" \
|
||||
"$registry:$RELEASE_VERSION-amd64" "$registry:$RELEASE_VERSION-arm64"
|
||||
done
|
||||
fi
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split GoReleaser builds across runners without requiring GoReleaser Pro."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import yaml
|
||||
|
||||
FULL_CONFIG = Path('.goreleaser.yaml')
|
||||
SIMPLE_CONFIG = Path('.goreleaser.simple.yaml')
|
||||
VERSION_FILE = Path('backend/cmd/server/VERSION')
|
||||
VERSION_RE = re.compile(r'\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?')
|
||||
|
||||
|
||||
def config(simple=False):
|
||||
return yaml.safe_load((SIMPLE_CONFIG if simple else FULL_CONFIG).read_text())
|
||||
|
||||
|
||||
def targets(simple=False):
|
||||
build = config()['builds'][0]
|
||||
result = []
|
||||
for goos, goarch in itertools.product(build['goos'], build['goarch']):
|
||||
item = {'goos': goos, 'goarch': goarch}
|
||||
if any(all(item.get(k) == v for k, v in rule.items()) for rule in build.get('ignore', [])):
|
||||
continue
|
||||
if not simple or item == {'goos': 'linux', 'goarch': 'amd64'}:
|
||||
result.append(item)
|
||||
if not result:
|
||||
raise ValueError('empty release target matrix')
|
||||
return result
|
||||
|
||||
|
||||
def archive_name(version, target):
|
||||
if not VERSION_RE.fullmatch(version) or target not in targets():
|
||||
raise ValueError('invalid release version or target')
|
||||
suffix = 'zip' if target['goos'] == 'windows' else 'tar.gz'
|
||||
return f"sub2api_{version}_{target['goos']}_{target['goarch']}.{suffix}"
|
||||
|
||||
|
||||
def sha256(path):
|
||||
with path.open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
def plan(args):
|
||||
sha = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip()
|
||||
if args.dry_run:
|
||||
version = VERSION_FILE.read_text().strip()
|
||||
tag = 'v' + version
|
||||
else:
|
||||
tag = args.ref
|
||||
version = tag.removeprefix('v')
|
||||
if not tag.startswith('v') or not VERSION_RE.fullmatch(version):
|
||||
raise ValueError('publishing requires a v-prefixed release version tag')
|
||||
tagged_sha = subprocess.check_output(['git', 'rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'], text=True).strip()
|
||||
if sha != tagged_sha:
|
||||
raise ValueError('checkout does not match the selected release tag')
|
||||
if not VERSION_RE.fullmatch(version):
|
||||
raise ValueError('invalid VERSION')
|
||||
VERSION_FILE.write_text(version + '\n')
|
||||
result = {'sha': sha, 'tag': tag, 'version': version,
|
||||
'owner_lower': os.environ.get('GITHUB_REPOSITORY_OWNER', '').lower(),
|
||||
'simple': str(args.simple).lower(), 'dry_run': str(args.dry_run).lower(),
|
||||
'date': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||
'matrix': json.dumps({'include': targets(args.simple)}, separators=(',', ':'))}
|
||||
with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
|
||||
for key, value in result.items():
|
||||
output.write(f'{key}={value}\n')
|
||||
|
||||
|
||||
def generate_config(args):
|
||||
data = config(args.simple if args.mode == 'publish' else False)
|
||||
data['snapshot'] = {'version_template': '{{ .Env.RELEASE_VERSION }}'}
|
||||
data['dockers'] = []
|
||||
data['docker_manifests'] = []
|
||||
if args.mode == 'build':
|
||||
target = {'goos': args.goos, 'goarch': args.goarch}
|
||||
if target not in targets():
|
||||
raise ValueError('unsupported build target')
|
||||
for build in data['builds']:
|
||||
build['goos'], build['goarch'], build['ignore'] = [args.goos], [args.goarch], []
|
||||
build['ldflags'] = [re.sub(r'{{\s*\.Date\s*}}', '{{ .Env.RELEASE_DATE }}', flag)
|
||||
for flag in build.get('ldflags', [])]
|
||||
else:
|
||||
# Artifacts are supplied through the OSS extra_files mechanism. No build
|
||||
# is repeated on the publishing runner, and release templates stay intact.
|
||||
data['before'] = {'hooks': []}
|
||||
data['builds'] = [{'id': 'sub2api', 'skip': True}]
|
||||
data['archives'] = []
|
||||
extra = [{'glob': 'release-input/sub2api_*.tar.gz'}, {'glob': 'release-input/sub2api_*.zip'}]
|
||||
if args.simple:
|
||||
data['checksum'] = {'disable': True}
|
||||
else:
|
||||
data['release']['extra_files'] = extra
|
||||
data['checksum'] = {'name_template': 'checksums.txt', 'algorithm': 'sha256', 'extra_files': extra}
|
||||
Path(args.output).write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
|
||||
|
||||
|
||||
def collect(args):
|
||||
target = {'goos': args.goos, 'goarch': args.goarch}
|
||||
name = archive_name(args.version, target)
|
||||
source = Path('dist') / name
|
||||
checksums = {line.split()[1].lstrip('*'): line.split()[0] for line in Path('dist/checksums.txt').read_text().splitlines()}
|
||||
digest = sha256(source)
|
||||
if checksums.get(name) != digest:
|
||||
raise ValueError('archive does not match the build checksum')
|
||||
output = Path(args.output)
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, output / name)
|
||||
manifest = {'sha': args.sha, 'version': args.version, 'target': target, 'archive': name, 'sha256': digest}
|
||||
(output / f"manifest-{args.goos}-{args.goarch}.json").write_text(json.dumps(manifest) + '\n')
|
||||
|
||||
|
||||
def verify(args):
|
||||
directory = Path(args.input)
|
||||
expected = set()
|
||||
for target in targets(args.simple):
|
||||
name = archive_name(args.version, target)
|
||||
manifest_name = f"manifest-{target['goos']}-{target['goarch']}.json"
|
||||
expected.update((name, manifest_name))
|
||||
manifest = json.loads((directory / manifest_name).read_text())
|
||||
if manifest != {'sha': args.sha, 'version': args.version, 'target': target,
|
||||
'archive': name, 'sha256': sha256(directory / name)}:
|
||||
raise ValueError(f'build provenance or checksum mismatch: {name}')
|
||||
if {p.name for p in directory.iterdir()} != expected:
|
||||
raise ValueError('missing or unexpected release artifacts')
|
||||
|
||||
|
||||
def contexts(args):
|
||||
verify(args)
|
||||
for target in targets(args.simple):
|
||||
if target['goos'] != 'linux':
|
||||
continue
|
||||
dest = Path(args.output) / target['goarch']
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
with tarfile.open(Path(args.input) / archive_name(args.version, target), 'r:gz') as archive:
|
||||
members = [member for member in archive.getmembers() if member.name in ('sub2api', './sub2api')]
|
||||
if len(members) != 1 or not members[0].isfile():
|
||||
raise ValueError('archive must contain one regular sub2api binary')
|
||||
with archive.extractfile(members[0]) as source, (dest / 'sub2api').open('wb') as output:
|
||||
shutil.copyfileobj(source, output)
|
||||
(dest / 'sub2api').chmod(0o755)
|
||||
shutil.copy2('Dockerfile.goreleaser', dest / 'Dockerfile')
|
||||
(dest / 'deploy').mkdir(exist_ok=True)
|
||||
shutil.copy2('deploy/docker-entrypoint.sh', dest / 'deploy/docker-entrypoint.sh')
|
||||
shutil.copytree('backend/resources', dest / 'backend/resources', dirs_exist_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest='command', required=True)
|
||||
p = commands.add_parser('plan')
|
||||
p.add_argument('--ref', required=True)
|
||||
p.add_argument('--simple', action='store_true')
|
||||
p.add_argument('--dry-run', action='store_true')
|
||||
p.set_defaults(run=plan)
|
||||
p = commands.add_parser('config')
|
||||
p.add_argument('mode', choices=['build', 'publish'])
|
||||
p.add_argument('--goos')
|
||||
p.add_argument('--goarch')
|
||||
p.add_argument('--simple', action='store_true')
|
||||
p.add_argument('--output', required=True)
|
||||
p.set_defaults(run=generate_config)
|
||||
p = commands.add_parser('collect')
|
||||
for arg in ('version', 'sha', 'goos', 'goarch', 'output'):
|
||||
p.add_argument('--' + arg, required=True)
|
||||
p.set_defaults(run=collect)
|
||||
for command, handler in [('verify', verify), ('contexts', contexts)]:
|
||||
p = commands.add_parser(command)
|
||||
for arg in ('version', 'sha', 'input'):
|
||||
p.add_argument('--' + arg, required=True)
|
||||
p.add_argument('--simple', action='store_true')
|
||||
if command == 'contexts':
|
||||
p.add_argument('--output', required=True)
|
||||
p.set_defaults(run=handler)
|
||||
args = parser.parse_args()
|
||||
args.run(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
PyYAML==6.0.3
|
||||
@@ -0,0 +1,191 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
spec = importlib.util.spec_from_file_location('release_matrix', Path(__file__).with_name('release_matrix.py'))
|
||||
release = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(release)
|
||||
|
||||
|
||||
class ReleaseMatrixTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.previous = Path.cwd()
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
os.chdir(self.temp.name)
|
||||
self.addCleanup(os.chdir, self.previous)
|
||||
for name in ('.goreleaser.yaml', '.goreleaser.simple.yaml'):
|
||||
shutil.copyfile(ROOT / name, name)
|
||||
Path('backend/cmd/server').mkdir(parents=True)
|
||||
release.VERSION_FILE.write_text('9.8.7\n')
|
||||
|
||||
def fixture_artifacts(self, simple=False):
|
||||
directory = Path('release-input')
|
||||
directory.mkdir()
|
||||
for target in release.targets(simple):
|
||||
name = release.archive_name('9.8.7', target)
|
||||
archive = directory / name
|
||||
if target['goos'] == 'linux':
|
||||
with tarfile.open(archive, 'w:gz') as out:
|
||||
info = tarfile.TarInfo('sub2api')
|
||||
info.size = 7
|
||||
info.mode = 0o755
|
||||
out.addfile(info, io.BytesIO(b'fixture'))
|
||||
else:
|
||||
archive.write_bytes(b'fixture archive')
|
||||
metadata = {'version': '9.8.7', 'sha': 'a' * 40, 'target': target,
|
||||
'archive': name, 'sha256': release.sha256(archive)}
|
||||
(directory / f"manifest-{target['goos']}-{target['goarch']}.json").write_text(json.dumps(metadata))
|
||||
return argparse.Namespace(input='release-input', version='9.8.7', sha='a' * 40, simple=simple, output='contexts')
|
||||
|
||||
def test_full_and_simple_matrix_match_existing_targets(self):
|
||||
full = release.targets()
|
||||
self.assertEqual(len(full), 5)
|
||||
self.assertNotIn({'goos': 'windows', 'goarch': 'arm64'}, full)
|
||||
self.assertEqual(release.targets(True), [{'goos': 'linux', 'goarch': 'amd64'}])
|
||||
|
||||
def test_leaf_keeps_packaging_and_selects_only_one_target(self):
|
||||
original = release.config()
|
||||
release.generate_config(argparse.Namespace(mode='build', simple=False, goos='darwin', goarch='arm64', output='leaf.yaml'))
|
||||
leaf = yaml.safe_load(Path('leaf.yaml').read_text())
|
||||
self.assertEqual(leaf['builds'][0]['goos'], ['darwin'])
|
||||
self.assertEqual(leaf['builds'][0]['goarch'], ['arm64'])
|
||||
self.assertEqual(leaf['builds'][0]['ignore'], [])
|
||||
self.assertEqual(leaf['archives'], original['archives'])
|
||||
self.assertEqual(leaf['release'], original['release'])
|
||||
self.assertFalse(leaf['dockers'])
|
||||
self.assertIn('{{ .Env.RELEASE_DATE }}', '\n'.join(leaf['builds'][0]['ldflags']))
|
||||
|
||||
def test_publication_config_has_no_compilation_or_docker_work(self):
|
||||
for simple in (False, True):
|
||||
with self.subTest(simple=simple):
|
||||
original = release.config(simple)
|
||||
release.generate_config(argparse.Namespace(mode='publish', simple=simple, output='publisher.yaml'))
|
||||
data = yaml.safe_load(Path('publisher.yaml').read_text())
|
||||
self.assertTrue(data['builds'][0]['skip'])
|
||||
self.assertFalse(data['archives'])
|
||||
self.assertFalse(data['dockers'])
|
||||
self.assertEqual(data['release']['header'], original['release']['header'])
|
||||
self.assertEqual(data['release']['footer'], original['release']['footer'])
|
||||
if simple:
|
||||
self.assertTrue(data['checksum']['disable'])
|
||||
self.assertTrue(data['release']['skip_upload'])
|
||||
else:
|
||||
self.assertEqual(data['checksum']['extra_files'], data['release']['extra_files'])
|
||||
|
||||
def test_collect_and_verify_hash_and_source_binding(self):
|
||||
args = self.fixture_artifacts()
|
||||
release.verify(args)
|
||||
file = next(Path(args.input).glob('*.tar.gz'))
|
||||
file.write_bytes(b'corrupted')
|
||||
with self.assertRaisesRegex(ValueError, 'checksum mismatch'):
|
||||
release.verify(args)
|
||||
|
||||
def test_missing_extra_and_wrong_commit_artifacts_are_rejected(self):
|
||||
args = self.fixture_artifacts(True)
|
||||
args.sha = 'b' * 40
|
||||
with self.assertRaises(ValueError):
|
||||
release.verify(args)
|
||||
args.sha = 'a' * 40
|
||||
Path('release-input/unexpected').write_text('not an asset')
|
||||
with self.assertRaises(ValueError):
|
||||
release.verify(args)
|
||||
Path('release-input/unexpected').unlink()
|
||||
next(Path('release-input').glob('*.tar.gz')).unlink()
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
release.verify(args)
|
||||
|
||||
def test_linux_context_preserves_binary_executable_mode(self):
|
||||
args = self.fixture_artifacts()
|
||||
Path('Dockerfile.goreleaser').write_text('FROM scratch\nCOPY sub2api /sub2api\n')
|
||||
Path('deploy').mkdir()
|
||||
Path('deploy/docker-entrypoint.sh').write_text('#!/bin/sh\nexec /app/sub2api\n')
|
||||
Path('backend/resources').mkdir()
|
||||
Path('backend/resources/data').write_text('fixture')
|
||||
release.contexts(args)
|
||||
for arch in ('amd64', 'arm64'):
|
||||
binary = Path('contexts') / arch / 'sub2api'
|
||||
self.assertEqual(binary.read_bytes(), b'fixture')
|
||||
self.assertEqual(binary.stat().st_mode & 0o777, 0o755)
|
||||
|
||||
def test_plan_requires_a_tag_for_publication(self):
|
||||
args = argparse.Namespace(ref='main', dry_run=False, simple=False)
|
||||
with patch.object(subprocess, 'check_output', return_value='a' * 40 + '\n'):
|
||||
with self.assertRaisesRegex(ValueError, 'version tag'):
|
||||
release.plan(args)
|
||||
args.ref = 'v9.8.7'
|
||||
with patch.object(subprocess, 'check_output', side_effect=['a' * 40 + '\n', 'b' * 40 + '\n']):
|
||||
with self.assertRaisesRegex(ValueError, 'does not match'):
|
||||
release.plan(args)
|
||||
|
||||
def test_dry_run_plan_resolves_matrix_without_a_new_tag(self):
|
||||
with patch.dict(os.environ, {'GITHUB_OUTPUT': 'outputs', 'GITHUB_REPOSITORY_OWNER': 'ExampleOwner'}), patch.object(subprocess, 'check_output', return_value='a' * 40 + '\n'):
|
||||
release.plan(argparse.Namespace(ref='feature/matrix', dry_run=True, simple=False))
|
||||
output = dict(line.split('=', 1) for line in Path('outputs').read_text().splitlines())
|
||||
self.assertEqual(output['dry_run'], 'true')
|
||||
self.assertEqual(output['owner_lower'], 'exampleowner')
|
||||
self.assertEqual(len(json.loads(output['matrix'])['include']), 5)
|
||||
|
||||
def test_docker_commands_do_not_publish_during_dry_run(self):
|
||||
fake_bin = Path('bin')
|
||||
fake_bin.mkdir()
|
||||
docker = fake_bin / 'docker'
|
||||
docker.write_text('#!/bin/sh\nprintf "%s\\n" "$*" >> "$DOCKER_LOG"\n')
|
||||
docker.chmod(0o755)
|
||||
env = {**os.environ, 'PATH': str(fake_bin.resolve()) + os.pathsep + os.environ['PATH'],
|
||||
'DOCKER_LOG': str(Path('docker.log').resolve()), 'RUNNER_TEMP': self.temp.name,
|
||||
'RELEASE_VERSION': '9.8.7', 'RELEASE_SHA': 'a' * 40, 'GITHUB_REPOSITORY': 'ExampleOwner/sub2api',
|
||||
'DRY_RUN': 'true', 'SIMPLE_RELEASE': 'false', 'DOCKERHUB_USERNAME': 'skip'}
|
||||
subprocess.run(['bash', str(ROOT / '.github/release-tools/release-images.sh')], env=env, check=True)
|
||||
log = Path('docker.log').read_text()
|
||||
self.assertEqual(log.count('buildx build'), 2)
|
||||
self.assertIn('linux/arm64', log)
|
||||
self.assertNotIn('--push', log)
|
||||
self.assertNotIn('imagetools', log)
|
||||
self.assertNotIn('skip/sub2api', log)
|
||||
self.assertIn('ghcr.io/exampleowner/sub2api', log)
|
||||
|
||||
|
||||
def test_published_full_and_simple_image_tags(self):
|
||||
fake_bin = Path('bin')
|
||||
fake_bin.mkdir()
|
||||
docker = fake_bin / 'docker'
|
||||
docker.write_text('#!/bin/sh\nprintf "%s\\n" "$*" >> "$DOCKER_LOG"\n')
|
||||
docker.chmod(0o755)
|
||||
for simple in (False, True):
|
||||
with self.subTest(simple=simple):
|
||||
log_path = Path(f'docker-{simple}.log').resolve()
|
||||
env = {**os.environ, 'PATH': str(fake_bin.resolve()) + os.pathsep + os.environ['PATH'],
|
||||
'DOCKER_LOG': str(log_path), 'RUNNER_TEMP': self.temp.name,
|
||||
'RELEASE_VERSION': '9.8.7', 'RELEASE_SHA': 'a' * 40, 'GITHUB_REPOSITORY': 'ExampleOwner/sub2api',
|
||||
'DRY_RUN': 'false', 'SIMPLE_RELEASE': str(simple).lower(), 'DOCKERHUB_USERNAME': 'fixturehub'}
|
||||
subprocess.run(['bash', str(ROOT / '.github/release-tools/release-images.sh')], env=env, check=True)
|
||||
log = log_path.read_text()
|
||||
self.assertIn('--push', log)
|
||||
self.assertEqual(log.count('buildx build'), 1 if simple else 2)
|
||||
if simple:
|
||||
self.assertNotIn('fixturehub', log)
|
||||
self.assertNotIn('imagetools', log)
|
||||
self.assertIn('ghcr.io/exampleowner/sub2api:latest', log)
|
||||
else:
|
||||
self.assertEqual(log.count('imagetools create'), 2)
|
||||
self.assertIn('fixturehub/sub2api:9.8', log)
|
||||
self.assertIn('ghcr.io/exampleowner/sub2api:9', log)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -25,6 +25,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check Docker Compose simple mode environment
|
||||
run: /bin/sh deploy/tests/docker-compose-simple-mode-env-test.sh
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
@@ -80,3 +82,14 @@ jobs:
|
||||
version: v2.13
|
||||
args: --timeout=30m
|
||||
working-directory: backend
|
||||
|
||||
release-helpers:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python -m pip install -r .github/release-tools/requirements-release.txt
|
||||
- run: python -m unittest discover -s .github/release-tools -p 'test_release_matrix.py'
|
||||
- run: bash -n .github/release-tools/release-images.sh
|
||||
|
||||
+216
-114
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to release (e.g., v1.0.0)'
|
||||
description: 'Tag to release (or a branch/ref for dry run)'
|
||||
required: true
|
||||
type: string
|
||||
simple_release:
|
||||
@@ -15,179 +15,286 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
dry_run:
|
||||
description: 'Build and verify all selected artifacts without publishing'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# 环境变量:合并 workflow_dispatch 输入和 repository variable
|
||||
# tag push 触发时读取 vars.SIMPLE_RELEASE,workflow_dispatch 时使用输入参数
|
||||
env:
|
||||
SIMPLE_RELEASE: ${{ github.event.inputs.simple_release == 'true' || vars.SIMPLE_RELEASE == 'true' }}
|
||||
concurrency:
|
||||
group: release-${{ github.repository }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SIMPLE_RELEASE: ${{ inputs.simple_release == true || vars.SIMPLE_RELEASE == 'true' }}
|
||||
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
|
||||
|
||||
jobs:
|
||||
# Update VERSION file with tag version
|
||||
update-version:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
sha: ${{ steps.plan.outputs.sha }}
|
||||
owner_lower: ${{ steps.plan.outputs.owner_lower }}
|
||||
tag: ${{ steps.plan.outputs.tag }}
|
||||
version: ${{ steps.plan.outputs.version }}
|
||||
date: ${{ steps.plan.outputs.date }}
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
simple: ${{ steps.plan.outputs.simple }}
|
||||
dry_run: ${{ steps.plan.outputs.dry_run }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout workflow tooling
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Update VERSION file
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
- name: Preserve tooling for releases of older tags
|
||||
run: cp -R .github/release-tools "$RUNNER_TEMP/release-tools"
|
||||
- name: Checkout selected application source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
|
||||
- name: Resolve one source commit and target matrix
|
||||
id: plan
|
||||
env:
|
||||
REQUESTED_REF: ${{ inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION=${{ github.event.inputs.tag }}
|
||||
VERSION=${VERSION#v}
|
||||
else
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
fi
|
||||
echo "$VERSION" > backend/cmd/server/VERSION
|
||||
echo "Updated VERSION file to: $VERSION"
|
||||
|
||||
- name: Upload VERSION artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
args=(--ref "$REQUESTED_REF")
|
||||
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
|
||||
if [[ "$DRY_RUN" == true ]]; then args+=(--dry-run); fi
|
||||
python "$RUNNER_TEMP/release-tools/release_matrix.py" plan "${args[@]}"
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-tooling
|
||||
path: ${{ runner.temp }}/release-tools/
|
||||
retention-days: 1
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: version-file
|
||||
path: backend/cmd/server/VERSION
|
||||
retention-days: 1
|
||||
|
||||
build-frontend:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.sha }}
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
cache: pnpm
|
||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: pnpm install --frozen-lockfile
|
||||
working-directory: frontend
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm run build
|
||||
- run: pnpm run build
|
||||
working-directory: frontend
|
||||
|
||||
- name: Upload frontend artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: backend/internal/web/dist/
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
needs: [update-version, build-frontend]
|
||||
build-binaries:
|
||||
needs: [prepare, build-frontend]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
|
||||
env:
|
||||
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
RELEASE_DATE: ${{ needs.prepare.outputs.date }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.sha }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Download VERSION artifact
|
||||
uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-tooling
|
||||
path: ${{ runner.temp }}/release-tools/
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: version-file
|
||||
path: backend/cmd/server/
|
||||
|
||||
- name: Download frontend artifact
|
||||
uses: actions/download-artifact@v8
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: backend/internal/web/dist/
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: false
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
cache: false
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.27.0'
|
||||
run: go version | grep -q 'go1.27.0'
|
||||
- name: Restore target-specific Go cache
|
||||
id: go-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: release-go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('backend/go.mod', 'backend/go.sum') }}-${{ needs.prepare.outputs.sha }}
|
||||
restore-keys: |
|
||||
release-go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('backend/go.mod', 'backend/go.sum') }}-
|
||||
release-go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.goos }}-${{ matrix.goarch }}-
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
|
||||
- name: Select one GoReleaser target
|
||||
env:
|
||||
TARGET_OS: ${{ matrix.goos }}
|
||||
TARGET_ARCH: ${{ matrix.goarch }}
|
||||
run: python "$RUNNER_TEMP/release-tools/release_matrix.py" config build --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output "$RUNNER_TEMP/goreleaser.matrix.yaml"
|
||||
- name: Build and archive without publishing
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: '~> v2'
|
||||
args: release --snapshot --clean --skip=validate,publish,docker,announce --config=${{ runner.temp }}/goreleaser.matrix.yaml
|
||||
env:
|
||||
GITHUB_REPO_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPO_OWNER_LOWER: ${{ needs.prepare.outputs.owner_lower }}
|
||||
GITHUB_REPO_NAME: ${{ github.event.repository.name }}
|
||||
DOCKERHUB_USERNAME: skip
|
||||
TAG_MESSAGE: ''
|
||||
- name: Verify and collect the target archive
|
||||
env:
|
||||
TARGET_OS: ${{ matrix.goos }}
|
||||
TARGET_ARCH: ${{ matrix.goarch }}
|
||||
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
|
||||
run: python "$RUNNER_TEMP/release-tools/release_matrix.py" collect --version "$RELEASE_VERSION" --sha "$RELEASE_SHA" --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output matrix-output
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-binary-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: matrix-output/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: Save updated target cache
|
||||
if: ${{ steps.go-cache.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ steps.go-cache.outputs.cache-primary-key }}
|
||||
|
||||
# Docker setup for GoReleaser
|
||||
release:
|
||||
needs: [prepare, build-binaries]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
env:
|
||||
SIMPLE_RELEASE: ${{ needs.prepare.outputs.simple }}
|
||||
DRY_RUN: ${{ needs.prepare.outputs.dry_run }}
|
||||
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
||||
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.sha }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-tooling
|
||||
path: ${{ runner.temp }}/release-tools/
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: release-binary-*
|
||||
path: release-input
|
||||
merge-multiple: true
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
|
||||
- name: Verify complete matrix and prepare Docker contexts
|
||||
run: |
|
||||
args=(--version "$RELEASE_VERSION" --sha "$RELEASE_SHA" --input release-input --output .release-context)
|
||||
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
|
||||
python "$RUNNER_TEMP/release-tools/release_matrix.py" contexts "${args[@]}"
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
cache: false
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
if: ${{ env.DRY_RUN != 'true' && env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
|
||||
uses: docker/login-action@v3
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Fetch tags with annotations
|
||||
run: |
|
||||
# 确保获取完整的 annotated tag 信息
|
||||
git fetch --tags --force
|
||||
|
||||
- name: Get tag message
|
||||
- name: Build images and publish manifests
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'skip' }}
|
||||
run: bash "$RUNNER_TEMP/release-tools/release-images.sh"
|
||||
- name: Get annotated tag notes
|
||||
id: tag_message
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG_NAME=${{ github.event.inputs.tag }}
|
||||
else
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
echo "Processing tag: $TAG_NAME"
|
||||
|
||||
# 获取完整的 tag message(跳过第一行标题)
|
||||
TAG_MESSAGE=$(git tag -l --format='%(contents:body)' "$TAG_NAME")
|
||||
|
||||
# 调试输出
|
||||
echo "Tag message length: ${#TAG_MESSAGE}"
|
||||
echo "Tag message preview:"
|
||||
echo "$TAG_MESSAGE" | head -10
|
||||
|
||||
# 使用 EOF 分隔符处理多行内容
|
||||
echo "message<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$TAG_MESSAGE" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set lowercase owner for GHCR
|
||||
id: lowercase
|
||||
run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run GoReleaser
|
||||
TAG_MESSAGE=$(git tag -l --format='%(contents:body)' "$RELEASE_TAG")
|
||||
DELIMITER="TAG_MESSAGE_$(openssl rand -hex 16)"
|
||||
{
|
||||
echo "message<<$DELIMITER"
|
||||
echo "$TAG_MESSAGE"
|
||||
echo "$DELIMITER"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Prepare publication-only GoReleaser config
|
||||
run: |
|
||||
args=(publish --output "$RUNNER_TEMP/goreleaser.publisher.yaml")
|
||||
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
|
||||
python "$RUNNER_TEMP/release-tools/release_matrix.py" config "${args[@]}"
|
||||
- name: Publish existing archives and release notes
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: '~> v2'
|
||||
args: release --clean --skip=validate ${{ env.SIMPLE_RELEASE == 'true' && '--config=.goreleaser.simple.yaml' || '' }}
|
||||
args: release --clean --skip=validate,before,docker ${{ env.DRY_RUN == 'true' && '--snapshot --skip=publish,announce' || '' }} --config=${{ runner.temp }}/goreleaser.publisher.yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_CURRENT_TAG: ${{ needs.prepare.outputs.tag }}
|
||||
TAG_MESSAGE: ${{ steps.tag_message.outputs.message }}
|
||||
GITHUB_REPO_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPO_OWNER_LOWER: ${{ steps.lowercase.outputs.owner }}
|
||||
GITHUB_REPO_NAME: ${{ github.event.repository.name }}
|
||||
GITHUB_REPO_OWNER_LOWER: ${{ needs.prepare.outputs.owner_lower }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'skip' }}
|
||||
- name: Upload dry-run checksums and metadata
|
||||
if: ${{ env.DRY_RUN == 'true' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-dry-run-report
|
||||
path: |
|
||||
dist/checksums.txt
|
||||
dist/artifacts.json
|
||||
dist/metadata.json
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# Update DockerHub description
|
||||
- name: Update DockerHub description
|
||||
if: ${{ env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
|
||||
if: ${{ env.DRY_RUN != 'true' && env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
|
||||
uses: peter-evans/dockerhub-description@v5
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -200,8 +307,9 @@ jobs:
|
||||
|
||||
# Send Telegram notification
|
||||
- name: Send Telegram Notification
|
||||
if: ${{ env.SIMPLE_RELEASE != 'true' }}
|
||||
if: ${{ env.DRY_RUN != 'true' && env.SIMPLE_RELEASE != 'true' }}
|
||||
env:
|
||||
TAG_MESSAGE: ${{ steps.tag_message.outputs.message }}
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -213,17 +321,12 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG_NAME=${{ github.event.inputs.tag }}
|
||||
else
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
TAG_NAME="$RELEASE_TAG"
|
||||
VERSION=${TAG_NAME#v}
|
||||
REPO="${{ github.repository }}"
|
||||
GHCR_IMAGE="ghcr.io/${REPO,,}" # ${,,} converts to lowercase
|
||||
|
||||
# 获取 tag message 内容并转义 Markdown 特殊字符
|
||||
TAG_MESSAGE='${{ steps.tag_message.outputs.message }}'
|
||||
TAG_MESSAGE=$(echo "$TAG_MESSAGE" | sed 's/\([_*`\[]\)/\\\1/g')
|
||||
|
||||
# 限制消息长度(Telegram 消息限制 4096 字符,预留空间给头尾固定内容)
|
||||
@@ -273,9 +376,13 @@ jobs:
|
||||
}')"
|
||||
|
||||
sync-version-file:
|
||||
needs: [release]
|
||||
if: ${{ needs.release.result == 'success' }}
|
||||
needs: [prepare, release]
|
||||
if: ${{ needs.release.result == 'success' && needs.prepare.outputs.dry_run != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v6
|
||||
@@ -284,12 +391,7 @@ jobs:
|
||||
|
||||
- name: Sync VERSION file to released tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION=${{ github.event.inputs.tag }}
|
||||
VERSION=${VERSION#v}
|
||||
else
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
fi
|
||||
VERSION="$RELEASE_VERSION"
|
||||
|
||||
CURRENT_VERSION=$(tr -d '\r\n' < backend/cmd/server/VERSION || true)
|
||||
if [ "$CURRENT_VERSION" = "$VERSION" ]; then
|
||||
|
||||
@@ -141,17 +141,21 @@ docs/*
|
||||
!docs/COMPOSITE_GROUPS.md
|
||||
!docs/PLUGIN_DEVELOPMENT.md
|
||||
!docs/channel-monitor-v2-safe-defaults.md
|
||||
!docs/codex-credits.md
|
||||
!docs/legal/
|
||||
!docs/legal/*.md
|
||||
!docs/screenshots/
|
||||
docs/screenshots/*
|
||||
!docs/screenshots/mobile-account-actions-menu.png
|
||||
!docs/screenshots/codex-referral-dialog.png
|
||||
!docs/screenshots/codex-referral-success.png
|
||||
.serena/
|
||||
.codex/
|
||||
frontend/coverage/
|
||||
aicodex
|
||||
output/
|
||||
.codegraph/
|
||||
issues/
|
||||
|
||||
# Vitest / Vite cache at repo root
|
||||
.vite/
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
.PHONY: build build-backend build-frontend test test-backend test-frontend test-frontend-critical
|
||||
|
||||
FRONTEND_CRITICAL_VITEST := \
|
||||
src/i18n/__tests__/localeKeyCompleteness.spec.ts \
|
||||
src/api/__tests__/client.spec.ts \
|
||||
src/api/__tests__/tokenRefresh.spec.ts \
|
||||
src/api/__tests__/keys.bulkUpdate.spec.ts \
|
||||
src/components/account/__tests__/OpenAIReferralCell.spec.ts \
|
||||
src/components/account/__tests__/OpenAIReferralCell.transport.spec.ts \
|
||||
src/components/account/__tests__/OpenAIQuotaResetCell.spark_shadow.spec.ts \
|
||||
src/components/keys/__tests__/BulkEditKeysModal.spec.ts \
|
||||
src/components/admin/user/__tests__/UserPlatformQuotaModal.spec.ts \
|
||||
src/views/user/__tests__/KeysView.spec.ts \
|
||||
src/api/__tests__/channelMonitorV2.spec.ts \
|
||||
src/views/auth/__tests__/LinuxDoCallbackView.spec.ts \
|
||||
src/views/auth/__tests__/WechatCallbackView.spec.ts \
|
||||
|
||||
@@ -49,8 +49,8 @@ Please read the following carefully before using this project:
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://apikey.fun/register?aff=SUB2API"><img src="assets/partners/logos/apikey-fun.png" alt="APIKEY.FUN" width="150"></a></td>
|
||||
<td>Thanks to APIKEY.FUN for sponsoring this project! <a href="https://apikey.fun/register?aff=SUB2API">APIKEY.FUN</a> is one of the core contributors to the sub2api open-source project, dedicated to providing open, stable, and cost-effective AI API access. The platform supports API relay services for Claude, OpenAI, Gemini, and other popular models, with pricing starting from as low as 7% of the original rate. Register via the exclusive link: <a href="https://apikey.fun/register?aff=SUB2API">APIKEY</a> to enjoy up to 5% off on all recharges.</td>
|
||||
<td width="180"><a href="https://apikey.fan/register?aff=SUB2API"><img src="assets/partners/logos/apikey-fun.png" alt="APIKEY.FUN" width="150"></a></td>
|
||||
<td>Thanks to APIKEY.FUN for sponsoring this project! <a href="https://apikey.fan/register?aff=SUB2API">APIKEY.FUN</a> is one of the core contributors to the sub2api open-source project, dedicated to providing open, stable, and cost-effective AI API access. The platform supports API relay services for Claude, OpenAI, Gemini, and other popular models, with pricing starting from as low as 7% of the original rate. Register via the exclusive link: <a href="https://apikey.fan/register?aff=SUB2API">APIKEY</a> to enjoy up to 5% off on all recharges.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -58,6 +58,11 @@ Please read the following carefully before using this project:
|
||||
<td>Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for sub2api users: if you register via <a href="https://aigocode.com/invite/SUB2API">this link</a>, you'll receive an extra 10% bonus credit on your first top-up!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://codex-everywhere.com"><img src="assets/partners/logos/codex-everywhere.jpg" alt="CodexEverywhere" width="150"></a></td>
|
||||
<td>Real GPT-5.6 series at 3% of OpenAI pricing — <a href="https://codex-everywhere.com">CodexEverywhere</a> is democratizing access to frontier models for developers worldwide. We believe in transparency and honesty, with model quality verified by active community oversight for months. USD and crypto friendly. Start with a free $20 trial at <a href="https://codex-everywhere.com">codex-everywhere.com</a>.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://shop.bmoplus.com/?utm_source=github"><img src="assets/partners/logos/bmoplus.jpg" alt="bmoplus" width="150"></a></td>
|
||||
<td>Huge thanks to BmoPlus for sponsoring this project! BmoPlus is a highly reliable AI account provider built strictly for heavy AI users and developers. They offer rock-solid, ready-to-use accounts and official top-up services for ChatGPT Plus / ChatGPT Pro (Full Warranty) / Claude Pro / Super Grok / Gemini Pro. By registering and ordering through <a href="https://shop.bmoplus.com/?utm_source=github">BmoPlus - Premium AI Accounts & Top-ups</a>, users can unlock the mind-blowing rate of 10% of the official GPT subscription price (90% OFF)</td>
|
||||
@@ -91,26 +96,12 @@ Please read the following carefully before using this project:
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
|
||||
<td>Thanks to Suixiang AI Gateway for sponsoring this project! <a href="https://sui-xiang.com/">Suixiang AI Gateway</a> is a reliable and efficient API relay service provider offering relay services for Claude, Codex, Gemini, and more. A privacy-focused relay — no data reselling, no model dilution; privacy, transparency, and lightning-fast after-sales support. New accounts get ¥0.5 in trial credit daily by signing in; top-ups are 1:1, no subscription required, pay-as-you-go. Multi-line redundancy, cross-region disaster recovery, automatic failover, and uninterrupted long-link SSE. 99.9% availability — critical calls never fall behind.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.proxy4free.com/?keyword=4yjqecpc"><img src="assets/partners/logos/proxy4free.png" alt="proxy4free" width="150"></a></td>
|
||||
<td>Thanks to Proxy4Free for sponsoring this project! Proxy4Free is a data proxy service provider for developers and AI applications, offering residential proxies, static residential proxies, ISP proxies, and datacenter proxies for scenarios such as Web Scraping, Browser Automation, and AI Agents. With global IP resources, stable connections, and flexible switching, it helps developers improve data collection success rates and reduce the risk of IP bans. Register via <a href="https://www.proxy4free.com/?keyword=4yjqecpc">this link</a> to get started and easily build more stable and efficient automation workflows.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="http://www.fastaitoken.com/register"><img src="assets/partners/logos/fastaitoken.jpg" alt="fastaitoken" width="150"></a></td>
|
||||
<td>🎉 Thanks to FastAIToken for sponsoring this project! <a href="http://www.fastaitoken.com/register">FastAIToken</a> is an AI API aggregation platform for developers, supporting mainstream large models such as OpenAI, Claude, and Gemini. Top-up at 1:1 — 1 CNY = 1 USD of API credit — letting developers use the world's leading large model services at lower cost and with greater convenience.<br>
|
||||
|
||||
🚀 The platform offers a variety of channels to choose from: an ultra-low-price 0.02x OpenAI promotional group (limited time), groups as low as 0.25x OpenAI, 0.7x Claude with 95% fixed cache, and a 1.2x Claude Max channel. It also provides a public status page showing real-time availability, latency, and operating status of each group for transparent and reliable service, plus 7×24 human technical support (not bots) with fast responses to developer needs.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="http://aimzoon.com"><img src="assets/partners/logos/aimzoon.jpg" alt="aimzoon" width="150"></a></td>
|
||||
<td>Thanks to Aimzoon for sponsoring this project! <a href="http://aimzoon.com">Aimzoon</a> provides stable, cost-effective AI API access services, enabling developers to quickly connect popular AI services to coding tools such as Codex, Claude Code, and Gemini CLI. No complex configuration — faster onboarding, more stable calls, and lower costs. Ongoing promotions including discounted Codex rates and special pricing, with free trial credits upon registration, bringing AI coding into your daily workflow. <a href="http://aimzoon.com">Click here</a> to register and try it out!
|
||||
@@ -122,11 +113,6 @@ Please read the following carefully before using this project:
|
||||
<td><a href="https://nagora.ai/">Nagora</a> is a multi-model AI API gateway built for developers and teams. With a single account and API key, you can access more than 26 leading text and image models through one unified interface. It is compatible with OpenAI, Anthropic, and Gemini protocols and integrates seamlessly with development tools such as Claude Code, Codex, and Gemini CLI. The platform provides intelligent routing, automatic failover, transparent pricing, and consolidated billing, along with budget management, rate limiting, and concurrency controls. This makes AI usage more reliable and manageable across individual development, team collaboration, and production environments. No changes to your existing application are required. Simply replace the Base URL and API key to complete the integration in as little as one minute.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.novada.com/?sub2api/"><img src="assets/partners/logos/novada.png" alt="Novada" width="150"></a></td>
|
||||
<td>Thanks to <a href="https://www.novada.com/?sub2api/">Novada</a> for sponsoring this project! Novada provides residential, ISP, datacenter, and mobile proxies, along with Web Unlocker and Scraper APIs for developers building AI applications and automation workflows. With global IP coverage, flexible rotating and sticky sessions, and precise geo-targeting, Novada helps teams access web data reliably for AI agent workflows, cross-region testing, web research, and browser automation. Explore Novada to build more stable and scalable AI workflows.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://s.qiniu.com/u6rQrq"><img src="assets/partners/logos/qiniu.jpg" alt="Qiniu AI" width="150"></a></td>
|
||||
<td>Thanks to Qiniu AI for sponsoring this project! Qiniu AI is the enterprise-grade large-model MaaS platform under Qiniu Cloud (02567.HK), offering one-stop access to 150+ mainstream models worldwide, compatible with the protocols of major global model providers, and covering full-modality capabilities including text, image, audio, video, and file processing, serving over 1.69 million enterprises and developers. Qiniu AI offers an exclusive benefit for Sub2API users: register via <a href="https://s.qiniu.com/u6rQrq">this link</a> — enterprise users get 12 million tokens free, and developers get 3 million tokens free.</td>
|
||||
@@ -167,6 +153,21 @@ Please read the following carefully before using this project:
|
||||
<td>Thanks to APIMart for sponsoring this project! <a href="https://go.apimart.ai/gh-sub2api">APIMart</a> is a low-cost API platform for AI image and video generation — GPT-Image-2 from $0.006 per image, with 160+ images per dollar. One async API covers both image and video: submit a task, get an ID, and retrieve results via polling or callback. Batch tens of thousands of images without timeouts, and switch models without changing code. Pay as you go with no monthly fee — <a href="https://go.apimart.ai/gh-sub2api">sign up here</a> to get started.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.axisnow.io/"><img src="assets/partners/logos/axisnow.jpg" alt="AxisNow" width="150"></a></td>
|
||||
<td>Thanks to AxisNow for sponsoring this project! <a href="https://www.axisnow.io/">AxisNow</a> protects and accelerates websites and APIs, delivering an optimal access experience across mainland China and globally, while extending acceleration and security capabilities to native/mobile apps through client SDKs — <strong>self-hosted private-deployment CDN</strong> | <strong>subscription-based DDoS-protected CDN</strong> | <strong>autonomous, flexibly composable CDN network</strong>.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://pp.dog/register?aff=SUB2API"><img src="assets/partners/logos/ppdog.png" alt="PP.dog" width="150"></a></td>
|
||||
<td><a href="https://pp.dog/register?aff=SUB2API">PP.dog</a> is a source-level API gateway with its own massive account pool, purpose-built as an upstream relay for downstream transit stations and high-frequency developers — saving you the hassle of building and maintaining your own account pool. ✅ Direct from source: self-operated account pool with no middleman markup; 🧧 Cost crusher: combined rate multiplier as low as 0.03x, just 0.35% of official pricing; 🚀 Blazing fast: first-token latency < 1s, on par with the official native API. <a href="https://www.pp.dog/register?aff=SUB2API">Get started with PP.dog now</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api"><img src="assets/partners/logos/cola-proxy.jpg" alt="ColaProxy" width="150"></a></td>
|
||||
<td>ColaProxy provides high-quality residential proxies built for web scraping, automation, and multi-account management. Get a free trial with non-expiring traffic, pricing starting as low as $0.3/GB, unlimited concurrent connections, and intelligent IP rotation for a smoother, more stable proxy experience. Use discount code COLA10 to get 10% off and start scaling your projects with reliable residential proxies today. <a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api">Get started with ColaProxy</a></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## Overview
|
||||
@@ -724,7 +725,10 @@ go generate ./cmd/server
|
||||
Simple Mode is designed for individual developers or internal teams who want quick access without full SaaS features.
|
||||
|
||||
- Enable: Set environment variable `RUN_MODE=simple`
|
||||
- Default groups are seeded on each startup. Set `SIMPLE_MODE_AUTO_CREATE_DEFAULT_GROUPS=false` (or YAML `simple_mode.auto_create_default_groups: false`) to manage groups yourself. The default is `true`; disabling it does not delete existing groups or change runtime auto-binding or admin concurrency setup.
|
||||
- Difference: Hides SaaS-related features and skips billing process
|
||||
- Optional key windows: Set `SIMPLE_MODE_KEY_RATE_LIMIT_ENABLED=true` to enforce each API key's configured 5-hour, daily, and 7-day spending windows. The default is `false`; balance and subscription debit remain bypassed when enabled.
|
||||
- Window enforcement uses the database as its source of truth and records only API-key window usage. It is a post-request soft cap, so concurrent in-flight requests can overshoot by their final costs. Historical simple-mode usage is not backfilled.
|
||||
- Security note: In production, you must also set `SIMPLE_MODE_CONFIRM=true` to allow startup
|
||||
|
||||
---
|
||||
@@ -817,12 +821,37 @@ xAI quota is passive. Sub2API does not invent subscription quota values; it reco
|
||||
|
||||
`401` responses temporarily remove accounts with invalid credentials from scheduling. `403` responses are treated as access or entitlement failures instead of token-refresh loops. `429` responses use `Retry-After` or a short cooldown to temporarily remove the account from scheduling.
|
||||
|
||||
New Grok image and video generation requests use a media-specific eligibility check. API-key accounts remain eligible. OAuth accounts require positive paid-entitlement evidence from the xAI billing probe; Free, forbidden, missing, malformed, and inconclusive billing observations are excluded from new media generation. Unobserved OAuth accounts are probed before the first media request is forwarded, and imports run the billing-first quota probe proactively. Chat requests and video status lookups are not affected by this media-only quarantine. If no eligible account remains, the media endpoint returns HTTP `503` with error type `grok_media_no_eligible_account`.
|
||||
New Grok image and video generation requests use a media-specific eligibility check. API-key accounts remain eligible. OAuth accounts with explicit Free or forbidden billing evidence are excluded from new media generation. Missing or malformed observations are probed before dispatch; a successful but incomplete billing response is treated as `billing_inconclusive` and remains eligible for backwards compatibility, because an unknown billing schema is not proof that the account lacks media entitlement. Operators can quarantine a known-bad account with `extra.grok_media_eligible=false` or force-enable a verified account with `true`. Imports run the billing-first quota probe proactively. Chat requests and video status lookups are not affected by this media-only quarantine. If no eligible account remains, the media endpoint returns HTTP `503` with error type `grok_media_no_eligible_account`.
|
||||
|
||||
Administrators can override automatic media eligibility through the account create/update API by setting `extra.grok_media_eligible` to `false` (exclude) or `true` (force eligible). On update, set it to `null` to remove the override and return to automatic probe-based behavior; omitting the field preserves the current override. A weekly allowance period alone is not treated as a paid tier signal. Successful image responses must contain at least one actual image output; empty HTTP `200` responses trigger account failover instead of being counted and returned as successful generations.
|
||||
|
||||
---
|
||||
|
||||
## TypeSafe / Jev Support
|
||||
|
||||
Sub2API supports TypeSafe API-key accounts through Jev's native, non-streaming System One protocol.
|
||||
|
||||
- Platform: `typesafe`; account type: API Key
|
||||
- Default upstream: `https://api.typesafe.ai`
|
||||
- Public endpoint: `POST /v1/systemone`
|
||||
- Model: `jev-latest`, also returned by `/v1/models` for TypeSafe groups
|
||||
- Questions: `noul`, `choice`, and `score`
|
||||
|
||||
Requests and successful responses retain the native System One JSON structure. This endpoint is not compatible with Chat Completions, Responses, Anthropic Messages, or streaming clients.
|
||||
|
||||
Question validation follows the TypeSafe OpenAPI wire schema (also used by SDK v0.5.7). `instructions` may be omitted or `null` for all question types. Noul `criteria` may be omitted or `null`; its `true`/`false` descriptions and Choice descriptions accept strings, objects, arrays, or `null`. Score `criteria` must be a non-empty array of string, object, or array descriptions; a single level is valid. SDK integer-keyed Score maps are normalized to arrays by the SDK before sending.
|
||||
|
||||
```bash
|
||||
curl https://your-sub2api.example.com/v1/systemone \
|
||||
-H 'Authorization: Bearer sk-your-sub2api-key' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"model":"jev-latest","state":"Text to evaluate","questions":{"safety":{"type":"noul","instructions":"Evaluate whether the text is unsafe"}}}'
|
||||
```
|
||||
|
||||
The built-in `jev-latest` price is `$0.042` per million input tokens and `$0` for output tokens. Channel pricing can override both values. Credential, billing, permission, rate-limit, overload, server, and network failures (`401`, `402`, `403`, `429`, `529`, `5xx`, transport errors) use the existing account error policy (including custom error codes and temporary-unschedulable rules) and fail over to another account; request errors (`400`, `413`, and `422`) are returned without retrying another account and never change account state. TypeSafe groups (and Composite requests routed to TypeSafe) reject Messages, Chat Completions, Responses, and count_tokens requests with `404`.
|
||||
|
||||
---
|
||||
|
||||
## Antigravity Support
|
||||
|
||||
Sub2API supports [Antigravity](https://antigravity.so/) accounts. After authorization, dedicated endpoints are available for Claude and Gemini models.
|
||||
|
||||
+82
-21
@@ -50,8 +50,8 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://apikey.fun/register?aff=SUB2API"><img src="assets/partners/logos/apikey-fun.png" alt="APIKEY.FUN" width="150"></a></td>
|
||||
<td>感谢 APIKEY.FUN 赞助了本项目!<a href="https://apikey.fun/register?aff=SUB2API">APIKEY.FUN</a> 是 sub2api 开源项目的核心贡献者之一,致力于提供开放、稳定、高性价比的 AI API 接入服务。平台支持 Claude、OpenAI、Gemini 等热门模型的 API 中转服务,价格低至官方原价的 7%。通过专属链接 <a href="https://apikey.fun/register?aff=SUB2API">APIKEY</a> 注册,可享受充值最高 95 折优惠。</td>
|
||||
<td width="180"><a href="https://apikey.fan/register?aff=SUB2API"><img src="assets/partners/logos/apikey-fun.png" alt="APIKEY.FUN" width="150"></a></td>
|
||||
<td>感谢 APIKEY.FUN 赞助了本项目!<a href="https://apikey.fan/register?aff=SUB2API">APIKEY.FUN</a> 是 sub2api 开源项目的核心贡献者之一,致力于提供开放、稳定、高性价比的 AI API 接入服务。平台支持 Claude、OpenAI、Gemini 等热门模型的 API 中转服务,价格低至官方原价的 7%。通过专属链接 <a href="https://apikey.fan/register?aff=SUB2API">APIKEY</a> 注册,可享受充值最高 95 折优惠。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -59,6 +59,11 @@
|
||||
<td>感谢 AIGoCode 赞助了本项目!AIGoCode 是一站式集成 Claude Code、Codex 以及最新 Gemini 模型的综合平台,为您提供稳定、高效、高性价比的 AI 编程服务。平台提供灵活的订阅方案,零封号风险,免 VPN 直连,响应极速。AIGoCode 为 sub2api 用户准备了专属福利:通过<a href="https://aigocode.com/invite/SUB2API">此链接</a>注册,首次充值可额外获得 10% 赠送额度!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://codex-everywhere.com"><img src="assets/partners/logos/codex-everywhere.jpg" alt="CodexEverywhere" width="150"></a></td>
|
||||
<td>Real GPT-5.6 series at 3% of OpenAI pricing — <a href="https://codex-everywhere.com">CodexEverywhere</a> is democratizing access to frontier models for developers worldwide. We believe in transparency and honesty, with model quality verified by active community oversight for months. USD and crypto friendly. Start with a free $20 trial at <a href="https://codex-everywhere.com">codex-everywhere.com</a>.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://shop.bmoplus.com/?utm_source=github"><img src="assets/partners/logos/bmoplus.jpg" alt="bmoplus" width="150"></a></td>
|
||||
<td>感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过<a href="https://shop.bmoplus.com/?utm_source=github">BmoPlus AI成品号专卖/代充</a>注册下单的用户,可享GPT 官网订阅一折 的震撼价格!</td>
|
||||
@@ -94,26 +99,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
|
||||
<td>感谢 随想AI网关 赞助本项目!<a href="https://sui-xiang.com/">随想AI网关</a> 是一家可靠高效的 API 中继服务提供商,提供 Claude、Codex、Gemini 等的中继服务。注重隐私的中转站·无数据倒卖·无模型掺水,隐私,透明,极速售后。新账户注册每日签到就送 0.5 元测试额度,充值额度 1:1,无需订阅,按量付费。多线路冗余、跨区域容灾、自动故障切换,长链路 SSE 不中断。99.9% 可用性,关键调用从不掉队。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.proxy4free.com/?keyword=4yjqecpc"><img src="assets/partners/logos/proxy4free.png" alt="proxy4free" width="150"></a></td>
|
||||
<td>感谢 Proxy4Free 赞助本项目!Proxy4Free 是面向开发者和 AI 应用的数据代理服务商,提供住宅代理、静态住宅代理、ISP 代理及数据中心代理等多种代理解决方案,适用于 Web Scraping、Browser Automation、AI Agent 等场景。支持全球 IP 资源、稳定连接与灵活切换,帮助开发者提升数据采集成功率,降低 IP 封禁风险。通过<a href="https://www.proxy4free.com/?keyword=4yjqecpc">此链接注册</a>即可开始体验,轻松构建更稳定、高效的自动化工作流。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="http://www.fastaitoken.com/register"><img src="assets/partners/logos/fastaitoken.jpg" alt="fastaitoken" width="150"></a></td>
|
||||
<td>🎉 感谢 FastAIToken 对本项目的赞助! <a href="http://www.fastaitoken.com/register">FastAIToken</a> 是面向开发者的 AI API 聚合平台,支持 OpenAI、Claude、Gemini 等主流大模型,充值 1:1,1 元 = 1 美元 API 额度,让开发者以更低成本、更便捷地使用全球领先的大模型服务。<br>
|
||||
|
||||
🚀 平台提供多种渠道自由选择:超级低价的0.02x OpenAI 福利分组(限时)、低至 0.25x OpenAI 分组、0.7x Claude 95%固定缓存、1.2x Claude Max 渠道;同时提供公开状态页,实时展示各分组的可用率、延迟及运行状态,服务透明可靠,并提供 7×24 小时真人技术支持(非机器人),快速响应开发者需求。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="http://aimzoon.com"><img src="assets/partners/logos/aimzoon.jpg" alt="aimzoon" width="150"></a></td>
|
||||
<td>感谢 Aimzoon 对本项目的赞助! <a href="http://aimzoon.com">Aimzoon</a> 提供稳定、高性价比的 AI API 接入服务,支持开发者将常用 AI 服务快速接入 Codex、Claude Code、Gemini CLI 等编程工具。无需复杂配置,更快接入,更稳调用,更省成本。codex倍率优惠,特价倍率等促销不断,注册即送免费体验额度,让 AI 编程真正进入日常工作流。<a href="http://aimzoon.com">点击这里</a>注册体验!
|
||||
@@ -125,11 +116,6 @@
|
||||
<td><a href="https://nagora.ai/">Nagora</a> 是专为开发者和团队打造的多模型 AI API 网关。通过一个账户和一枚 API Key,即可统一调用 26+ 款主流文本与图像模型,兼容 OpenAI、Anthropic 与 Gemini 协议,并可无缝接入 Claude Code、Codex、Gemini CLI 等开发工具。平台提供智能路由、自动故障转移、透明计费与统一账单,同时支持预算、限速、并发控制,让个人开发、团队协作和生产环境中的 AI 调用更稳定、更可控。无需改造现有应用,只需替换 Base URL 与 API Key,最快 1 分钟即可完成接入。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.novada.com/?sub2api/"><img src="assets/partners/logos/novada.png" alt="Novada" width="150"></a></td>
|
||||
<td>感谢 <a href="https://www.novada.com/?sub2api/">Novada</a> 赞助本项目!Novada 为构建 AI 应用与自动化工作流的开发者提供住宅代理、ISP 代理、数据中心代理与移动代理,以及 Web Unlocker 和 Scraper API。凭借全球 IP 覆盖、灵活的轮换与粘性会话以及精准的地理定位,Novada 帮助团队在 AI Agent 工作流、跨区域测试、网络调研与浏览器自动化等场景中稳定获取网络数据。立即体验 Novada,构建更稳定、更可扩展的 AI 工作流。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://s.qiniu.com/u6rQrq"><img src="assets/partners/logos/qiniu.jpg" alt="七牛云AI" width="150"></a></td>
|
||||
<td>感谢 七牛云AI 赞助本项目!七牛云AI 是七牛云(02567.HK)旗下企业级大模型 MaaS 平台,一站式调用全球 150+ 主流模型,兼容全球主流模型厂商协议,覆盖文本、图像、音频、视频、文件处理等全模态处理能力,服务超过169万企业及开发者用户。七牛云 AI 为 Sub2API 的用户提供了专属福利:通过<a href="https://s.qiniu.com/u6rQrq">此链接</a>注册,企业用户免费领1200万Token,开发者免费领300万Token。</td>
|
||||
@@ -170,6 +156,21 @@
|
||||
<td>感谢 APIMart 赞助了本项目!<a href="https://go.apimart.ai/gh-sub2api">APIMart</a> 是专注于 AI 图片/视频生成的低价 API 平台,GPT-Image-2 低至 $0.006/张,1 美元可生成 160+ 张图片。图片、视频一套异步 API 通吃:提交任务获取 ID,通过轮询或回调获取结果;批量生成上万张图片也不会超时,切换模型无需修改代码。按量付费、无月费,通过<a href="https://go.apimart.ai/gh-sub2api">此注册链接</a>注册即可开始使用。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.axisnow.io/"><img src="assets/partners/logos/axisnow.jpg" alt="AxisNow" width="150"></a></td>
|
||||
<td>感谢 AxisNow 赞助了本项目!<a href="https://www.axisnow.io/">AxisNow</a> 保护并加速网站与 API,兼顾中国大陆及全球的访问体验,并通过客户端 SDK,将加速与安全能力延伸至原生/移动 App — <strong>自建私有部署 CDN</strong>|<strong>订阅式高防 CDN</strong>|<strong>自主可控、灵活组合的 CDN 网络</strong>。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://pp.dog/register?aff=SUB2API"><img src="assets/partners/logos/ppdog.png" alt="PP.dog" width="150"></a></td>
|
||||
<td><a href="https://pp.dog/register?aff=SUB2API">PP.dog</a> 是自建账号池的源头 API 网关,专注为下游中转站与高频开发者提供 API 网关中继服务,帮您省去自建号池的一切麻烦——✅ 源头直供:自持海量账号池,无中间商赚差价;🧧 成本屠夫:综合倍率低至 0.03x,成本仅为官方的千分之3.5;🚀 极速体验:首 Token 延迟 < 1s,流畅媲美官方原生 API。<a href="https://www.pp.dog/register?aff=SUB2API">立即接入PP.dog</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api"><img src="assets/partners/logos/cola-proxy.jpg" alt="ColaProxy" width="150"></a></td>
|
||||
<td>ColaProxy 提供专为网页抓取、自动化和多账号管理打造的高质量住宅代理。免费试用,流量永不过期,价格低至 $0.3/GB,支持无限并发连接和智能 IP 轮换,带来更流畅、更稳定的代理体验。使用优惠码 COLA10 立享 9 折优惠,立即开始使用可靠的住宅代理扩展您的项目。<a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api">立即开始使用 ColaProxy</a></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## 项目概述
|
||||
@@ -218,6 +219,30 @@ underscores_in_headers on;
|
||||
|
||||
Nginx 默认会丢弃名称中含下划线的请求头(如 `session_id`),这会导致多账号环境下的粘性会话功能失效。
|
||||
|
||||
## Codex Fast/Flex 策略说明
|
||||
|
||||
管理员后台的 `系统设置 -> 网关服务 -> OpenAI Fast/Flex 策略` 只负责处理请求体中的 `service_tier`,不会修改 Codex 客户端的模型目录,也不会让 Codex UI 自动出现 Speed 或 `/fast` 选项。
|
||||
|
||||
策略支持以下处理方式:
|
||||
|
||||
- `pass`:保留客户端传入的 `service_tier`;`fast` 会规范为上游使用的 `priority`。
|
||||
- `filter`:移除 `service_tier`,按普通优先级请求。
|
||||
- `block`:拒绝匹配的 Fast/Flex 请求。
|
||||
- `force_priority`:将匹配请求强制设置为 `priority`,会按 Priority/Fast 价格计费。为避免升级后改变既有规则语义,`all` 只匹配显式存在的 tier;如需让省略 `service_tier` 的 OpenAI 请求也强制升级,必须新增 `service_tier=missing + force_priority` 规则。非 OpenAI 平台不会执行缺省 tier 注入。这种方式可以让请求实际使用 Fast,但 Codex UI 仍可能不显示 Fast 状态。
|
||||
|
||||
Codex 的 Fast 入口由客户端模型目录驱动。只有当前模型目录声明了 `additional_speed_tiers: ["fast"]` 和对应的 `service_tiers`,Codex 才会显示 `/fast`。通过 API Key 或自定义模型提供商连接 Sub2API 时,如果模型目录没有这些字段,即使后台配置了 `force_priority`,重启 Codex 后也不会出现 Speed 选项。
|
||||
|
||||
客户端可在 `~/.codex/config.toml` 中直接指定默认请求级别:
|
||||
|
||||
```toml
|
||||
service_tier = "fast"
|
||||
|
||||
[features]
|
||||
fast_mode = true
|
||||
```
|
||||
|
||||
其中 `service_tier = "fast"` 会让请求携带 Fast 设置;`features.fast_mode` 只启用客户端 Fast 功能。模型目录没有声明 Fast 能力时,`/fast` 仍可能不显示。可通过 Sub2API 使用记录确认最终 `service_tier` 是否为 `priority`。
|
||||
|
||||
---
|
||||
|
||||
## 部署方式
|
||||
@@ -697,16 +722,52 @@ go generate ./cmd/server
|
||||
|
||||
---
|
||||
|
||||
## OpenAI 图片模型
|
||||
|
||||
支持 `gpt-image-2.5-flare`、`gpt-image-2.5-sunburst` 及其 `2026-09-08` 日期快照,可通过 `/v1/images/generations`、`/v1/images/edits` 调用。`quality` 支持 `xhigh`、`max`、`auto`,合法自定义尺寸和图片 usage 明细保持透传。
|
||||
|
||||
OAuth / Setup Token 图片请求使用 Responses 主控模型调用 `image_generation` 工具,默认主控为 `gpt-5.6-luna`。可设置 `SUB2API_IMAGES_MAIN_MODEL` 切换为账号支持的文本模型;Docker Compose 用户修改 `.env` 后执行 `docker compose up -d` 重建容器。该配置不会替换所选图片模型,也不会覆盖 `/v1/responses` 请求中已经提供的文本主控模型。
|
||||
|
||||
升级后,无模型限制的账号自动支持新模型。已有显式账号映射或分组白名单需要加入两个 2.5 模型(日期快照按需加入);升级不会自动扩大管理员设置的模型权限。新模型内置价格包含官方文本输入、图片输入和图片输出 token 费率,远端价格表尚未更新时使用内置 2.5 价格;实际按次或按 token 计费仍由既有分组/渠道配置决定。
|
||||
|
||||
## 简易模式
|
||||
|
||||
简易模式适合个人开发者或内部团队快速使用,不依赖完整 SaaS 功能。
|
||||
|
||||
- 启用方式:设置环境变量 `RUN_MODE=simple`
|
||||
- 默认每次启动会补齐默认分组。设置 `SIMPLE_MODE_AUTO_CREATE_DEFAULT_GROUPS=false`(或 YAML `simple_mode.auto_create_default_groups: false`)可自行管理分组。默认值为 `true`;关闭后不删除已有分组,不改变运行时自动绑定或管理员并发设置。
|
||||
- 功能差异:隐藏 SaaS 相关功能,跳过计费流程
|
||||
- 可选密钥窗口:设置 `SIMPLE_MODE_KEY_RATE_LIMIT_ENABLED=true` 后,按每个 API Key 配置的 5 小时、1 天、7 天消费窗口进行限制,默认值为 `false`;启用后仍跳过余额和订阅扣费。
|
||||
- 窗口限制以数据库为准,只记录 API Key 窗口用量。它在请求完成后记账,并发中的请求可能以各自最终费用超过窗口上限。启用前的历史用量不会自动补算。
|
||||
- 安全注意事项:生产环境需同时设置 `SIMPLE_MODE_CONFIRM=true` 才允许启动
|
||||
|
||||
---
|
||||
|
||||
## TypeSafe / Jev 使用说明
|
||||
|
||||
Sub2API 支持使用 TypeSafe API Key 账户,通过 Jev 原生、非流式的 System One 协议调用模型。
|
||||
|
||||
- 平台:`typesafe`;账号类型:API Key
|
||||
- 默认上游:`https://api.typesafe.ai`
|
||||
- 对外端点:`POST /v1/systemone`
|
||||
- 模型:`jev-latest`,TypeSafe 分组的 `/v1/models` 也会返回该模型
|
||||
- 问题类型:`noul`、`choice`、`score`
|
||||
|
||||
请求和成功响应保持 System One 原生 JSON 结构。该端点不兼容 Chat Completions、Responses、Anthropic Messages 或流式客户端。
|
||||
|
||||
问题校验遵循 TypeSafe OpenAPI 的线上协议 schema(SDK v0.5.7 也使用该 schema)。所有问题的 `instructions` 都可以省略或为 `null`。Noul 的 `criteria` 可以省略或为 `null`,其中 `true`/`false` 的描述和 Choice 描述支持字符串、对象、数组或 `null`。Score 的 `criteria` 必须是至少包含一档描述的数组,每档支持字符串、对象或数组;单档也合法。SDK 的整数键 Score 映射会由 SDK 在发送前转换为数组。
|
||||
|
||||
```bash
|
||||
curl https://your-sub2api.example.com/v1/systemone \
|
||||
-H 'Authorization: Bearer sk-your-sub2api-key' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"model":"jev-latest","state":"待评估文本","questions":{"safety":{"type":"noul","instructions":"评估文本是否不安全"}}}'
|
||||
```
|
||||
|
||||
`jev-latest` 内置价格为输入 `$0.042/百万 tokens`、输出 `$0`,渠道定价可以覆盖。凭据、欠费、权限、限流、过载、服务端和网络错误(`401`、`402`、`403`、`429`、`529`、`5xx`、传输错误)沿用现有账号错误策略(含自定义错误码与临时不可调度规则)并切换账号;请求错误(`400`、`413`、`422`)不会切换账号重试,也不会改变账号状态。TypeSafe 分组(以及路由到 TypeSafe 的 Composite 请求)调用 Messages、Chat Completions、Responses、count_tokens 时返回 `404`。
|
||||
|
||||
---
|
||||
|
||||
## Antigravity 使用说明
|
||||
|
||||
Sub2API 支持 [Antigravity](https://antigravity.so/) 账户,授权后可通过专用端点访问 Claude 和 Gemini 模型。
|
||||
|
||||
+22
-21
@@ -49,8 +49,8 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://apikey.fun/register?aff=SUB2API"><img src="assets/partners/logos/apikey-fun.png" alt="APIKEY.FUN" width="150"></a></td>
|
||||
<td>APIKEY.FUN のご支援に感謝します!<a href="https://apikey.fun/register?aff=SUB2API">APIKEY.FUN</a> は sub2api オープンソースプロジェクトのコアコントリビューターの一つであり、オープンで安定した、コストパフォーマンスに優れた AI API アクセスサービスの提供に取り組んでいます。プラットフォームは Claude、OpenAI、Gemini など人気モデルの API 中継サービスをサポートし、価格は公式料金のわずか 7% から。専用リンク <a href="https://apikey.fun/register?aff=SUB2API">APIKEY</a> から登録すると、すべてのチャージで永久 5% 割引をご利用いただけます。</td>
|
||||
<td width="180"><a href="https://apikey.fan/register?aff=SUB2API"><img src="assets/partners/logos/apikey-fun.png" alt="APIKEY.FUN" width="150"></a></td>
|
||||
<td>APIKEY.FUN のご支援に感謝します!<a href="https://apikey.fan/register?aff=SUB2API">APIKEY.FUN</a> は sub2api オープンソースプロジェクトのコアコントリビューターの一つであり、オープンで安定した、コストパフォーマンスに優れた AI API アクセスサービスの提供に取り組んでいます。プラットフォームは Claude、OpenAI、Gemini など人気モデルの API 中継サービスをサポートし、価格は公式料金のわずか 7% から。専用リンク <a href="https://apikey.fan/register?aff=SUB2API">APIKEY</a> から登録すると、すべてのチャージで永久 5% 割引をご利用いただけます。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -58,6 +58,11 @@
|
||||
<td>AIGoCode のご支援に感謝します!AIGoCode は Claude Code、Codex、最新の Gemini モデルを統合したオールインワンプラットフォームで、安定的かつ効率的でコストパフォーマンスに優れた AI コーディングサービスを提供します。柔軟なサブスクリプションプラン、アカウント停止リスクゼロ、VPN 不要の直接アクセス、超高速レスポンスが特長です。AIGoCode は sub2api ユーザー向けに特別特典を用意しています:<a href="https://aigocode.com/invite/SUB2API">こちらのリンク</a>から登録すると、初回チャージ時に 10% のボーナスクレジットを追加プレゼント!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://codex-everywhere.com"><img src="assets/partners/logos/codex-everywhere.jpg" alt="CodexEverywhere" width="150"></a></td>
|
||||
<td>OpenAI 公式価格のわずか 3% で本物の GPT-5.6 シリーズを提供 — <a href="https://codex-everywhere.com">CodexEverywhere</a> は世界中の開発者にフロンティアモデルへのアクセスを民主化しています。私たちは透明性と誠実さを信条とし、モデル品質は数か月にわたるアクティブなコミュニティの監視によって検証されています。USD および暗号通貨に対応。<a href="https://codex-everywhere.com">codex-everywhere.com</a> で $20 の無料トライアルから始めましょう。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://shop.bmoplus.com/?utm_source=github"><img src="assets/partners/logos/bmoplus.jpg" alt="bmoplus" width="150"></a></td>
|
||||
<td>本プロジェクトにご支援いただいた BmoPlus に感謝いたします!BmoPlusは、AIサブスクリプションのヘビーユーザー向けに特化した信頼性の高いAIアカウントサービスプロバイダーであり、安定した ChatGPT Plus / ChatGPT Pro (完全保証) / Claude Pro / Super Grok / Gemini Pro の公式代行チャージおよび即納アカウントを提供しています。こちらの<a href="https://shop.bmoplus.com/?utm_source=github">BmoPlus AIアカウント専門店/代行チャージ</a>経由でご登録・ご注文いただいたユーザー様は、GPTを 公式サイト価格の約1割(90% OFF) という驚異的な価格でご利用いただけます!</td>
|
||||
@@ -93,26 +98,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
|
||||
<td>随想AI ゲートウェイのご支援に感謝します!<a href="https://sui-xiang.com/">随想AI ゲートウェイ</a> は信頼性と効率に優れた API 中継サービスプロバイダーで、Claude、Codex、Gemini などの中継サービスを提供しています。プライバシー重視の中継ステーション・データ転売なし・モデル水増しなし、プライバシー・透明性・超高速アフターサービス。新規アカウント登録後、毎日サインインで 0.5 元のテストクレジットがもらえ、チャージは 1:1、サブスクリプション不要、従量課金。マルチライン冗長、クロスリージョン災害復旧、自動フェイルオーバー、長時間 SSE 接続が途切れません。99.9% の可用性、重要な呼び出しは決して遅れません。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.proxy4free.com/?keyword=4yjqecpc"><img src="assets/partners/logos/proxy4free.png" alt="proxy4free" width="150"></a></td>
|
||||
<td>Proxy4Free のご支援に感謝します!Proxy4Free は開発者と AI アプリケーション向けのデータプロキシサービスプロバイダーで、住宅プロキシ、静的住宅プロキシ、ISP プロキシ、データセンタープロキシなど多様なプロキシソリューションを提供しており、Web Scraping、Browser Automation、AI Agent などのシナリオに適しています。グローバル IP リソース、安定した接続、柔軟な切り替えをサポートし、開発者のデータ収集成功率の向上と IP ブロックリスクの低減を支援します。<a href="https://www.proxy4free.com/?keyword=4yjqecpc">こちらのリンクから登録</a>して、より安定した効率的な自動化ワークフローを簡単に構築しましょう。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="http://www.fastaitoken.com/register"><img src="assets/partners/logos/fastaitoken.jpg" alt="fastaitoken" width="150"></a></td>
|
||||
<td>🎉 FastAIToken のご支援に感謝します!<a href="http://www.fastaitoken.com/register">FastAIToken</a> は開発者向けの AI API アグリゲーションプラットフォームで、OpenAI、Claude、Gemini などの主要な大規模モデルに対応しています。チャージは 1:1(1 元 = 1 米ドル分の API クレジット)で、開発者がより低コスト・より手軽に世界トップクラスの大規模モデルサービスを利用できます。<br>
|
||||
|
||||
🚀 プラットフォームでは多彩なチャネルを自由に選択できます:超低価格の 0.02x OpenAI 特典グループ(期間限定)、最低 0.25x の OpenAI グループ、0.7x Claude(95% 固定キャッシュ)、1.2x Claude Max チャネル。さらに、各グループの可用率・レイテンシ・稼働状況をリアルタイムで表示する公開ステータスページを提供し、透明で信頼できるサービスを実現。7×24 時間の有人テクニカルサポート(ボットではありません)により、開発者のニーズに迅速に対応します。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="http://aimzoon.com"><img src="assets/partners/logos/aimzoon.jpg" alt="aimzoon" width="150"></a></td>
|
||||
<td>Aimzoon のご支援に感謝します!<a href="http://aimzoon.com">Aimzoon</a> は安定してコストパフォーマンスに優れた AI API 接続サービスを提供し、開発者が主要な AI サービスを Codex、Claude Code、Gemini CLI などのコーディングツールへ素早く接続できるようにします。複雑な設定は不要で、より速い接続、より安定した呼び出し、より低いコストを実現。Codex レート割引や特価レートなどのキャンペーンも随時開催中、登録するだけで無料お試しクレジットをプレゼント。AI コーディングを日常のワークフローへ。<a href="http://aimzoon.com">こちら</a>から登録してお試しください!
|
||||
@@ -124,11 +115,6 @@
|
||||
<td><a href="https://nagora.ai/">Nagora</a>は、開発者やチーム向けに設計されたマルチモデルAI APIゲートウェイです。1つのアカウントと1つのAPIキーだけで、26種類以上の主要なテキストモデルおよび画像モデルを一元的に利用できます。OpenAI、Anthropic、Geminiの各プロトコルに対応し、Claude Code、Codex、Gemini CLIなどの開発ツールにもシームレスに接続できます。 プラットフォームには、インテリジェントルーティング、自動フェイルオーバー、透明性の高い料金体系、請求の一元管理に加え、予算管理、レート制限、同時実行数の制御機能が備わっています。これにより、個人開発、チームでの共同作業、本番環境におけるAI APIの利用を、より安定的かつ柔軟に管理できます。 既存のアプリケーションを改修する必要はありません。Base URLとAPIキーを置き換えるだけで、最短1分で導入を完了できます。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.novada.com/?sub2api/"><img src="assets/partners/logos/novada.png" alt="Novada" width="150"></a></td>
|
||||
<td>Novada のご支援に感謝します!<a href="https://www.novada.com/?sub2api/">Novada</a> は、AI アプリケーションや自動化ワークフローを構築する開発者向けに、レジデンシャル、ISP、データセンター、モバイルプロキシに加え、Web Unlocker と Scraper API を提供しています。グローバルな IP カバレッジ、柔軟なローテーション/スティッキーセッション、精密なジオターゲティングにより、AI エージェントワークフロー、クロスリージョンテスト、Web リサーチ、ブラウザ自動化などのシーンで、チームが Web データへ確実にアクセスできるよう支援します。Novada で、より安定しスケーラブルな AI ワークフローを構築しましょう。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://s.qiniu.com/u6rQrq"><img src="assets/partners/logos/qiniu.jpg" alt="Qiniu AI" width="150"></a></td>
|
||||
<td>七牛云AI のご支援に感謝します!七牛云AI は、七牛云(02567.HK)傘下のエンタープライズ向け大規模モデル MaaS プラットフォームです。世界の主要モデル 150+ をワンストップで利用でき、グローバル主要モデルプロバイダーのプロトコルに対応し、テキスト、画像、音声、動画、ファイル処理などのフルモーダル処理能力をカバー。169万を超える企業・開発者ユーザーにサービスを提供しています。Sub2API ユーザー向けの限定特典として、<a href="https://s.qiniu.com/u6rQrq">こちらのリンク</a>から登録すると、企業ユーザーは 1200万 Token、開発者は 300万 Token を無料で獲得できます。</td>
|
||||
@@ -169,6 +155,21 @@
|
||||
<td>APIMart のご支援に感謝します!<a href="https://go.apimart.ai/gh-sub2api">APIMart</a> は AI 画像・動画生成に特化した低価格 API プラットフォームです。GPT-Image-2 は 1 枚 $0.006 から、1 ドルで 160 枚以上の画像を生成できます。画像と動画の両方に対応する非同期 API を 1 つで利用でき、タスクを送信して ID を取得し、ポーリングまたはコールバックで結果を取得できます。数万枚規模のバッチ処理でもタイムアウトせず、モデルを変更してもコードの変更は不要です。月額料金なしの従量課金制で、<a href="https://go.apimart.ai/gh-sub2api">こちらの登録リンク</a>からすぐに利用を開始できます。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.axisnow.io/"><img src="assets/partners/logos/axisnow.jpg" alt="AxisNow" width="150"></a></td>
|
||||
<td>AxisNow のご支援に感謝します!<a href="https://www.axisnow.io/">AxisNow</a> はウェブサイトと API を保護・高速化し、中国本土および世界中で快適なアクセス体験を実現します。クライアント SDK により、ネイティブ/モバイルアプリにも高速化・セキュリティ機能を拡張できます — <strong>自社構築・プライベートデプロイ CDN</strong>|<strong>サブスクリプション型高防御 CDN</strong>|<strong>自律制御で柔軟に組み合わせられる CDN ネットワーク</strong>。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://pp.dog/register?aff=SUB2API"><img src="assets/partners/logos/ppdog.png" alt="PP.dog" width="150"></a></td>
|
||||
<td><a href="https://pp.dog/register?aff=SUB2API">PP.dog</a> は自社運用の大規模アカウントプールを持つソースレベルの API ゲートウェイで、下流の中継ステーションや高頻度開発者向けに API ゲートウェイリレーサービスを提供し、アカウントプールの自主構築にかかるすべての手間を省きます。✅ ソース直供:自社運用の大規模アカウントプール、中間マージンなし;🧧 コスト最強:総合レート倍率わずか 0.03x、コストは公式の 0.35% のみ;🚀 超高速体験:ファーストトークンレイテンシ 1 秒未満、公式ネイティブ API に匹敵するスムーズさ。<a href="https://www.pp.dog/register?aff=SUB2API">今すぐ PP.dog に接続</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api"><img src="assets/partners/logos/cola-proxy.jpg" alt="ColaProxy" width="150"></a></td>
|
||||
<td>ColaProxy は、Web スクレイピング、自動化、マルチアカウント管理向けに構築された高品質な住宅プロキシを提供します。有効期限なしのトラフィックで無料トライアルが利用でき、料金は $0.3/GB から、無制限の同時接続とインテリジェントな IP ローテーションにより、よりスムーズで安定したプロキシ体験を実現します。割引コード COLA10 で 10% オフ、信頼性の高い住宅プロキシでプロジェクトを今すぐスケールしましょう。<a href="https://colaproxy.com/?utm_source=sub2api&utm_medium=sub2api&ref=sub2api">ColaProxy を始める</a></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## 概要
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
@@ -1 +1 @@
|
||||
0.1.182
|
||||
0.2.12
|
||||
|
||||
@@ -99,6 +99,7 @@ func provideCleanup(
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
cnProviderBalanceCheck *service.CNProviderBalanceCheckService,
|
||||
codexVersionSync *service.OpenAICodexVersionSyncService,
|
||||
claudeCodeVersionSync *service.ClaudeCodeVersionSyncService,
|
||||
proxyExpiry *service.ProxyExpiryService,
|
||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||
usageCleanup *service.UsageCleanupService,
|
||||
@@ -124,6 +125,7 @@ func provideCleanup(
|
||||
quotaFlusher *service.UserPlatformQuotaUsageFlusher,
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService,
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService,
|
||||
opencodeGoUsage *service.OpenCodeGoUsageService,
|
||||
auditLog *service.AuditLogService,
|
||||
openAIAutoReset *service.OpenAIQuotaAutoResetService,
|
||||
promptAudit *securityaudit.PromptService,
|
||||
@@ -272,6 +274,10 @@ func provideCleanup(
|
||||
codexVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ClaudeCodeVersionSyncService", func() error {
|
||||
claudeCodeVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ProxyExpiryService", func() error {
|
||||
proxyExpiry.Stop()
|
||||
return nil
|
||||
@@ -380,6 +386,12 @@ func provideCleanup(
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpenCodeGoUsageService", func() error {
|
||||
if opencodeGoUsage != nil {
|
||||
opencodeGoUsage.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
}
|
||||
|
||||
infraSteps := []cleanupStep{
|
||||
|
||||
@@ -117,10 +117,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
openAI403CounterCache := repository.NewOpenAI403CounterCache(redisClient)
|
||||
geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
|
||||
compositeTokenCacheInvalidator := service.NewCompositeTokenCacheInvalidator(geminiTokenCache)
|
||||
rateLimitService := service.ProvideRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache, timeoutCounterCache, openAI403CounterCache, settingService, compositeTokenCacheInvalidator)
|
||||
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
||||
leaderLockCache := repository.NewLeaderLockCache(redisClient)
|
||||
ollamaCloudUsageService := service.ProvideOllamaCloudUsageService(accountRepository, httpUpstream, settingService, secretEncryptor, configConfig, leaderLockCache, db)
|
||||
rateLimitService := service.ProvideRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache, timeoutCounterCache, openAI403CounterCache, settingService, compositeTokenCacheInvalidator, ollamaCloudUsageService)
|
||||
identityCache := repository.NewIdentityCache(redisClient)
|
||||
identityService := service.NewIdentityService(identityCache)
|
||||
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
||||
timingWheelService, err := service.ProvideTimingWheelService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -137,7 +139,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
tlsFingerprintProfileCache := repository.NewTLSFingerprintProfileCache(redisClient)
|
||||
tlsFingerprintProfileService := service.NewTLSFingerprintProfileService(tlsFingerprintProfileRepository, tlsFingerprintProfileCache)
|
||||
channelRepository := repository.NewChannelRepository(db)
|
||||
channelService := service.NewChannelService(channelRepository, groupRepository, apiKeyAuthCacheInvalidator, pricingService)
|
||||
channelCachePubSub := repository.NewChannelCache(redisClient)
|
||||
channelService := service.NewChannelService(channelRepository, groupRepository, apiKeyAuthCacheInvalidator, pricingService, channelCachePubSub)
|
||||
modelPricingResolver := service.NewModelPricingResolver(channelService, billingService)
|
||||
compositeModelRouteRepository := repository.NewCompositeModelRouteRepository(client)
|
||||
compositeRouteResolver := service.NewCompositeRouteResolver(compositeModelRouteRepository)
|
||||
@@ -178,39 +181,40 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
channelMonitorUserHandler := handler.NewChannelMonitorUserHandler(channelMonitorService, settingService)
|
||||
channelMonitorV2Repository := repository.NewChannelMonitorV2Repository(db)
|
||||
channelMonitorV2Service := service.ProvideChannelMonitorV2Service(channelMonitorV2Repository, settingService)
|
||||
channelMonitorV2Handler := handler.NewChannelMonitorV2Handler(channelMonitorV2Service)
|
||||
channelMonitorV2Handler := handler.NewChannelMonitorV2Handler(channelMonitorV2Service, apiKeyService)
|
||||
dashboardAggregationRepository := repository.NewDashboardAggregationRepository(db)
|
||||
dashboardStatsCache := repository.NewDashboardCache(redisClient, configConfig)
|
||||
dashboardService := service.NewDashboardService(usageLogRepository, dashboardAggregationRepository, dashboardStatsCache, configConfig)
|
||||
leaderLockCache := repository.NewLeaderLockCache(redisClient)
|
||||
dashboardAggregationService := service.ProvideDashboardAggregationService(dashboardAggregationRepository, timingWheelService, leaderLockCache, db, configConfig)
|
||||
dashboardAggregationService := service.ProvideDashboardAggregationService(dashboardAggregationRepository, timingWheelService, leaderLockCache, db, configConfig, settingRepository)
|
||||
dashboardHandler := admin.NewDashboardHandler(dashboardService, dashboardAggregationService)
|
||||
adminGroupRepository := repository.NewAdminGroupRepository(client, db)
|
||||
adminAccountRepository := repository.NewAdminAccountRepository(client, db, schedulerCache)
|
||||
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
||||
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
||||
adminService := service.NewAdminService(userRepository, adminGroupRepository, adminAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, userRPMCache, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, openAIGatewayService, affiliateService, compositeModelRouteRepository, compositeRouteResolver, channelService)
|
||||
adminService := service.NewAdminService(configConfig, userRepository, adminGroupRepository, adminAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, userRPMCache, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, openAIGatewayService, affiliateService, compositeModelRouteRepository, compositeRouteResolver, channelService)
|
||||
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService, serviceUserPlatformQuotaRepository, billingCache, totpService, userService, settingService)
|
||||
groupCapacityService := service.NewGroupCapacityService(accountRepository, groupRepository, concurrencyService, sessionLimitCache, rpmCache)
|
||||
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService)
|
||||
groupHandler := admin.NewGroupHandlerWithConfig(adminService, dashboardService, groupCapacityService, configConfig)
|
||||
claudeUsageFetcher := repository.NewClaudeUsageFetcher(httpUpstream)
|
||||
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository, configConfig)
|
||||
grokQuotaFetcher := service.NewGrokQuotaFetcher()
|
||||
grokQuotaService := service.ProvideGrokQuotaService(accountRepository, proxyRepository, grokTokenProvider, httpUpstream, configConfig, usageLogRepository, settingService)
|
||||
openAIQuotaService := service.ProvideOpenAIQuotaService(accountRepository, proxyRepository, openAITokenProvider, privacyClientFactory, openAIGatewayService)
|
||||
openAIReferralClient := repository.NewOpenAIReferralClient(privacyClientFactory)
|
||||
openAIQuotaService := service.ProvideOpenAIQuotaService(accountRepository, proxyRepository, openAITokenProvider, privacyClientFactory, openAIReferralClient, openAIGatewayService)
|
||||
usageCache := service.NewUsageCache()
|
||||
accountUsageService := service.ProvideAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, grokQuotaFetcher, grokQuotaService, openAIQuotaService, usageCache, identityCache, tlsFingerprintProfileService, openAIGatewayService)
|
||||
pluginRepository := repository.NewPluginRepository(db)
|
||||
pluginHostInfo := providePluginHostInfo(buildInfo)
|
||||
pluginManager := service.NewPluginManager(pluginRepository, secretEncryptor, configConfig, pluginHostInfo)
|
||||
pluginKVStore := repository.NewPluginKVStore(redisClient)
|
||||
pluginManager := service.ProvidePluginManager(pluginRepository, secretEncryptor, configConfig, pluginHostInfo, pluginKVStore, openAIGatewayService)
|
||||
accountTestService := service.ProvideAccountTestService(accountRepository, geminiTokenProvider, claudeTokenProvider, grokTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService, openAIGatewayService, settingService, pluginManager)
|
||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||
accountHandler := admin.ProvideAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator, grokQuotaService)
|
||||
accountHandler := admin.ProvideAccountHandler(configConfig, adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator, grokQuotaService)
|
||||
adminAnnouncementHandler := admin.NewAnnouncementHandler(announcementService)
|
||||
dataManagementService := service.NewDataManagementService()
|
||||
dataManagementHandler := admin.NewDataManagementHandler(dataManagementService)
|
||||
backupObjectStoreFactory := repository.NewS3BackupStoreFactory()
|
||||
dbDumper := repository.NewPgDumper(configConfig)
|
||||
dbDumper := repository.NewPgDumper(configConfig, db)
|
||||
backupService := service.ProvideBackupService(settingRepository, configConfig, secretEncryptor, backupObjectStoreFactory, dbDumper, leaderLockCache, db)
|
||||
imageStorageFactory := repository.ProvideImageStorageFactory()
|
||||
imageStorageSettingService := service.ProvideImageStorageSettingService(settingRepository, secretEncryptor, backupService, imageStorageFactory, configConfig)
|
||||
@@ -283,15 +287,17 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
auditLogService := service.ProvideAuditLogService(auditLogRepository, settingService)
|
||||
auditLogHandler := admin.NewAuditLogHandler(auditLogService, totpService)
|
||||
upstreamBillingProbeService := service.ProvideUpstreamBillingProbeService(accountRepository, accountTestService, settingService, leaderLockCache, db)
|
||||
ollamaCloudUsageService := service.ProvideOllamaCloudUsageService(accountRepository, httpUpstream, settingService, secretEncryptor, configConfig, leaderLockCache, db)
|
||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, grokOAuthHandler, cnProviderHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, pluginHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, contentModerationHandler, promptAdminHandler, paymentHandler, affiliateHandler, complianceHandler, auditLogHandler, upstreamBillingProbeService, ollamaCloudUsageService)
|
||||
openCodeGoUsageService := service.ProvideOpenCodeGoUsageService(accountRepository, httpUpstream, settingService, leaderLockCache, db)
|
||||
idempotencyCoordinator := service.ProvideIdempotencyCoordinator(idempotencyRepository, configConfig)
|
||||
claudeResetCreditService := service.ProvideClaudeResetCreditService(accountRepository, claudeTokenProvider, proxyRepository, settingService, idempotencyCoordinator, leaderLockCache)
|
||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, grokOAuthHandler, cnProviderHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, pluginHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, contentModerationHandler, promptAdminHandler, paymentHandler, affiliateHandler, complianceHandler, auditLogHandler, upstreamBillingProbeService, ollamaCloudUsageService, openCodeGoUsageService, claudeResetCreditService)
|
||||
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
||||
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
|
||||
legacyEngine := securityaudit.NewLegacyModerationAdapter(contentModerationService)
|
||||
coordinator := securityaudit.NewCoordinator(legacyEngine, promptService)
|
||||
gatewayHandler := handler.ProvideGatewayHandler(gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, userMessageQueueService, configConfig, settingService, coordinator)
|
||||
openAIGatewayHandler := handler.ProvideOpenAIGatewayHandler(openAIGatewayService, pluginManager, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, opsService, grokQuotaService, configConfig, coordinator)
|
||||
openAIGatewayHandler := handler.ProvideOpenAIGatewayHandler(openAIGatewayService, pluginManager, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, opsService, grokQuotaService, configConfig, coordinator, compositeRouteResolver)
|
||||
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo, notificationEmailService)
|
||||
totpHandler := handler.NewTotpHandler(totpService)
|
||||
passkeyRepository := repository.NewPasskeyRepository(db)
|
||||
@@ -317,7 +323,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
batchImageDownloadService := service.NewBatchImageDownloadService(batchImageRepository, accountRepository, batchImageDownloadLimiter, configConfig)
|
||||
batchImageCleanupService := service.ProvideBatchImageCleanupService(batchImageRepository, accountRepository, configConfig)
|
||||
batchImageHandler := handler.ProvideBatchImageHandler(batchImagePublicService, batchImageDownloadService, batchImageCleanupService, openAIGatewayHandler)
|
||||
idempotencyCoordinator := service.ProvideIdempotencyCoordinator(idempotencyRepository, configConfig)
|
||||
idempotencyCleanupService := service.ProvideIdempotencyCleanupService(idempotencyRepository, configConfig)
|
||||
openAIQuotaAutoResetService := service.ProvideOpenAIQuotaAutoResetService(accountRepository, openAIQuotaService, rateLimitService, idempotencyCoordinator, auditLogService, settingService, leaderLockCache)
|
||||
handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, announcementHandler, channelMonitorUserHandler, channelMonitorV2Handler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler, totpHandler, passkeyHandler, handlerPaymentHandler, paymentWebhookHandler, availableChannelHandler, modelPlazaHandler, asyncImageHandler, batchImageHandler, idempotencyCoordinator, idempotencyCleanupService, openAIQuotaAutoResetService)
|
||||
@@ -338,6 +343,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
||||
cnProviderBalanceCheckService := service.ProvideCNProviderBalanceCheckService(accountRepository, cnProviderBalanceService, cnProviderQuotaService, configConfig)
|
||||
openAICodexVersionSyncService := service.ProvideOpenAICodexVersionSyncService(settingRepository, settingService, gitHubReleaseClient)
|
||||
claudeCodeVersionSyncService := service.ProvideClaudeCodeVersionSyncService(settingRepository, settingService, gitHubReleaseClient)
|
||||
proxyExpiryService := service.ProvideProxyExpiryService(proxyRepository)
|
||||
subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository, settingRepository, notificationEmailService, leaderLockCache, db)
|
||||
batchImageWorkerRuntime := service.ProvideBatchImageWorkerRuntime(batchImageRepository, accountRepository, batchImageQueue, usageBillingRepository, usageLogRepository, batchImageModelPricingResolver, apiKeyAuthCacheInvalidator, configConfig)
|
||||
@@ -347,7 +353,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService, settingService, channelMonitorQuotaFetcher)
|
||||
channelMonitorV2Aggregator := service.ProvideChannelMonitorV2Aggregator(channelMonitorV2Repository, db, settingService)
|
||||
userPlatformQuotaUsageFlusher := service.ProvideUserPlatformQuotaUsageFlusher(configConfig, billingCache, serviceUserPlatformQuotaRepository, timingWheelService)
|
||||
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, opsService, opsIngressRejectAggregator, apiKeyService, authCacheInvalidationWorker, schedulerSnapshotService, tokenRefreshService, accountExpiryService, cnProviderBalanceCheckService, openAICodexVersionSyncService, proxyExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, batchImageCleanupService, batchImageWorkerRuntime, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, openAIGatewayService, scheduledTestRunnerService, backupService, paymentOrderExpiryService, channelMonitorRunner, channelMonitorV2Aggregator, userPlatformQuotaUsageFlusher, upstreamBillingProbeService, ollamaCloudUsageService, auditLogService, openAIQuotaAutoResetService, promptService, pluginManager)
|
||||
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, opsService, opsIngressRejectAggregator, apiKeyService, authCacheInvalidationWorker, schedulerSnapshotService, tokenRefreshService, accountExpiryService, cnProviderBalanceCheckService, openAICodexVersionSyncService, claudeCodeVersionSyncService, proxyExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, batchImageCleanupService, batchImageWorkerRuntime, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, openAIGatewayService, scheduledTestRunnerService, backupService, paymentOrderExpiryService, channelMonitorRunner, channelMonitorV2Aggregator, userPlatformQuotaUsageFlusher, upstreamBillingProbeService, ollamaCloudUsageService, openCodeGoUsageService, auditLogService, openAIQuotaAutoResetService, promptService, pluginManager)
|
||||
application := &Application{
|
||||
Server: httpServer,
|
||||
PromptAudit: promptService,
|
||||
@@ -402,6 +408,7 @@ func provideCleanup(
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
cnProviderBalanceCheck *service.CNProviderBalanceCheckService,
|
||||
codexVersionSync *service.OpenAICodexVersionSyncService,
|
||||
claudeCodeVersionSync *service.ClaudeCodeVersionSyncService,
|
||||
proxyExpiry *service.ProxyExpiryService,
|
||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||
usageCleanup *service.UsageCleanupService,
|
||||
@@ -427,6 +434,7 @@ func provideCleanup(
|
||||
quotaFlusher *service.UserPlatformQuotaUsageFlusher,
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService,
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService,
|
||||
opencodeGoUsage *service.OpenCodeGoUsageService,
|
||||
auditLog *service.AuditLogService,
|
||||
openAIAutoReset *service.OpenAIQuotaAutoResetService,
|
||||
promptAudit *securityaudit.PromptService,
|
||||
@@ -574,6 +582,10 @@ func provideCleanup(
|
||||
codexVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ClaudeCodeVersionSyncService", func() error {
|
||||
claudeCodeVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ProxyExpiryService", func() error {
|
||||
proxyExpiry.Stop()
|
||||
return nil
|
||||
@@ -682,6 +694,12 @@ func provideCleanup(
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpenCodeGoUsageService", func() error {
|
||||
if opencodeGoUsage != nil {
|
||||
opencodeGoUsage.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
}
|
||||
|
||||
infraSteps := []cleanupStep{
|
||||
|
||||
@@ -41,6 +41,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
)
|
||||
accountExpirySvc := service.NewAccountExpiryService(nil, time.Second)
|
||||
codexVersionSyncSvc := service.NewOpenAICodexVersionSyncService(nil, nil, nil, time.Second)
|
||||
claudeCodeVersionSyncSvc := service.NewClaudeCodeVersionSyncService(nil, nil, nil, time.Second)
|
||||
proxyExpirySvc := service.NewProxyExpiryService(nil, time.Second)
|
||||
subscriptionExpirySvc := service.NewSubscriptionExpiryService(nil, time.Second)
|
||||
pricingSvc := service.NewPricingService(cfg, nil)
|
||||
@@ -68,6 +69,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
accountExpirySvc,
|
||||
nil, // cnProviderBalanceCheck
|
||||
codexVersionSyncSvc,
|
||||
claudeCodeVersionSyncSvc,
|
||||
proxyExpirySvc,
|
||||
subscriptionExpirySvc,
|
||||
&service.UsageCleanupService{},
|
||||
@@ -93,6 +95,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
nil, // quotaFlusher
|
||||
nil, // upstreamBillingProbe
|
||||
nil, // ollamaCloudUsage
|
||||
nil, // opencodeGoUsage
|
||||
nil, // auditLog
|
||||
nil, // openAIAutoReset
|
||||
nil, // promptAudit
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# Upstream error proxy attribution
|
||||
|
||||
## Scope
|
||||
|
||||
Gateway inference errors are stored in `ops_error_logs.upstream_errors`. Each
|
||||
array item represents one upstream attempt, including same-account retries and
|
||||
cross-account failover attempts. Proxy attribution is stored on that item; it
|
||||
must not be reconstructed later from `accounts.proxy_id`.
|
||||
|
||||
This repository has one gateway implementation under `backend/internal/service`.
|
||||
There is no separate `gateway/backend` source copy to synchronize.
|
||||
|
||||
## Event contract
|
||||
|
||||
Every newly persisted event contains these fields. `proxy_id` is emitted even
|
||||
when its value is JSON `null`:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `proxy_id` | Managed proxy ID used by the attempt; `null` for direct or unknown routes. |
|
||||
| `proxy_name` | Snapshotted proxy name, `direct/no_proxy`, or `unknown`. |
|
||||
|
||||
Only proxy ID and name are stored. Proxy URL, protocol endpoint, host, port,
|
||||
username, password, and authorization data are excluded.
|
||||
|
||||
The two sentinel names have strict meanings:
|
||||
|
||||
- `direct/no_proxy`: the transport was explicitly given no proxy.
|
||||
- `unknown`: the route cannot be proven from event-time evidence. This includes
|
||||
legacy events, pre-transport credential failures, and OpenAI WebSocket use of
|
||||
the default HTTP client.
|
||||
|
||||
Invariant: `proxy_id` is `null` if and only if `proxy_name` is one of the two
|
||||
sentinels. A managed proxy with a blank name (defensive only; `proxies.name` is
|
||||
non-empty) is labeled `proxy`. Both the struct normalizer used at append time
|
||||
and the JSON normalizer used on detail reads enforce this, so an event can never
|
||||
carry `proxy_id=null` together with a real proxy name.
|
||||
|
||||
## Why the account snapshot is event-time evidence
|
||||
|
||||
Account selection loads `ProxyID` and `Proxy` into one in-memory snapshot. HTTP,
|
||||
TLS-fingerprint, Bedrock, Gemini, Antigravity, OpenAI, and OpenAI WebSocket
|
||||
forwarding use that snapshot for managed proxy routing.
|
||||
Inference transports use a managed proxy only when both the binding ID and its
|
||||
hydrated proxy object are present; the attribution accessors use the same rule.
|
||||
OpenAI WebSocket additionally retains the existing default-client environment
|
||||
proxy behavior when that snapshot does not yield a usable proxy. The transport
|
||||
does not switch to a database backup proxy during a request.
|
||||
|
||||
Expired proxy fallback happens before scheduling by atomically rewriting the
|
||||
account binding:
|
||||
|
||||
- backup proxy: `proxy_id` becomes the backup ID and
|
||||
`proxy_fallback_origin_id` stores the expired proxy ID;
|
||||
- direct fallback: `proxy_id` becomes null and
|
||||
`proxy_fallback_origin_id` stores the expired proxy ID.
|
||||
|
||||
Custom Anthropic relays receive the same snapshotted proxy as their encoded
|
||||
relay proxy parameter. OpenAI WebSocket requests keep the existing transport
|
||||
behavior: configured account proxies use the proxy client, while an empty
|
||||
account proxy leaves the HTTP client unset so `coder/websocket` uses
|
||||
`http.DefaultClient`, including its `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`
|
||||
handling.
|
||||
|
||||
For WebSocket errors without a usable account proxy, the event stores
|
||||
`proxy_id=null` and `proxy_name=unknown`. It does not infer whether the default
|
||||
client selected an environment proxy or a direct connection after the fact.
|
||||
This applies to every event produced while a request is on the WebSocket
|
||||
transport, including WS handshake/fallback errors and the first-output timeout
|
||||
raised by the WS passthrough relay; `opsUpstreamWSProxyAttribution` is the
|
||||
accessor for those sites and never returns `direct/no_proxy`.
|
||||
|
||||
Credential acquisition failures that occur before the inference transport is
|
||||
opened are also recorded as unknown. The account's proxy binding is not treated
|
||||
as proof that the credential operation or an inference request used that route.
|
||||
|
||||
Each failure site constructs a complete `OpsUpstreamErrorEvent`. Its literal
|
||||
sets `proxy_id` and `proxy_name` from the same route input used by the attempt.
|
||||
Credential-free accessors copy only the managed proxy ID and name. A fallback
|
||||
proxy needs no special event flag: its event-time ID and name are the historical
|
||||
route evidence. Shared helpers that assemble the event on behalf of a caller
|
||||
(`handleUpstreamTransportError`, `handleOpenAIUpstreamTransportError`,
|
||||
`appendOpenAICompactFallbackRetryOps`, `recordOpenAIRawStreamTruncation`,
|
||||
`newOpenAIFirstOutputTimeoutError`) stamp attribution themselves, so a caller
|
||||
cannot forget it. Every non-test `OpsUpstreamErrorEvent{` literal in
|
||||
`backend/internal/service` must either set `ProxyName` or be passed to one of
|
||||
those helpers; grep for literals missing `ProxyName` before merging changes
|
||||
that add a failure site.
|
||||
|
||||
`appendOpsUpstreamError` accepts only the completed event. It does not receive
|
||||
an `Account`, query current account state, or infer proxy attribution while
|
||||
appending. Once appended, the event contains only scalar snapshot values, so
|
||||
later account mutation or failover cannot change an earlier event.
|
||||
|
||||
Queue sanitization keeps attempt events newest-first under hard bounds:
|
||||
|
||||
- the newest 16 attempts retain `detail` and `upstream_response_body` (each
|
||||
capped at the queue body limit) plus `upstream_url`/`message` up to 2048
|
||||
bytes;
|
||||
- older attempts keep timestamps, account, proxy, status, kind, stage, scope,
|
||||
reason and a `upstream_url`/`message` trimmed to 512 bytes; an attempt whose
|
||||
only payload was a cleared `detail` is still retained;
|
||||
- at most 256 attempts and roughly 512 KiB of serialized JSON are kept per
|
||||
request. When either bound drops attempts, the oldest retained event carries
|
||||
`dropped_earlier_attempts` with the number of discarded earlier attempts. The
|
||||
newest attempt is always retained because it decides the request outcome.
|
||||
|
||||
## Legacy events and analytics
|
||||
|
||||
Legacy JSON is not rewritten in the database. Detail reads edit only the
|
||||
`proxy_id`/`proxy_name` keys of events that lack valid attribution, so keys
|
||||
written by older struct versions and the stored key order are preserved. The
|
||||
shared parser applies the same rule to the decoded structs. Both materialize
|
||||
missing attribution as:
|
||||
|
||||
```json
|
||||
{
|
||||
"proxy_id": null,
|
||||
"proxy_name": "unknown"
|
||||
}
|
||||
```
|
||||
|
||||
Proxy and region aggregation must use the event fields first. A safe grouping
|
||||
rule is:
|
||||
|
||||
1. A non-null event `proxy_id` groups by that ID and uses the event
|
||||
`proxy_name` as the historical label. A join to `proxies` may enrich current
|
||||
metadata but must not replace the event label.
|
||||
2. `proxy_id=null` and `proxy_name=direct/no_proxy` groups as direct.
|
||||
3. Missing attribution, or `proxy_id=null` and `proxy_name=unknown`, groups as
|
||||
unknown. Do not join through the account's current `proxy_id` and present it
|
||||
as historical attribution.
|
||||
|
||||
If an operator deliberately produces a current-account cohort for legacy data,
|
||||
the report must be labeled as a current snapshot; it is not historical proxy
|
||||
attribution.
|
||||
@@ -182,6 +182,8 @@ const (
|
||||
ProviderKimi Provider = "kimi"
|
||||
ProviderZhipu Provider = "zhipu"
|
||||
ProviderDeepseek Provider = "deepseek"
|
||||
ProviderMinimax Provider = "minimax"
|
||||
ProviderOpencodeGo Provider = "opencode_go"
|
||||
)
|
||||
|
||||
func (pr Provider) String() string {
|
||||
@@ -191,7 +193,7 @@ func (pr Provider) String() string {
|
||||
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
|
||||
func ProviderValidator(pr Provider) error {
|
||||
switch pr {
|
||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek:
|
||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek, ProviderMinimax, ProviderOpencodeGo:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("channelmonitor: invalid enum value for provider field: %q", pr)
|
||||
|
||||
@@ -108,6 +108,8 @@ const (
|
||||
ProviderKimi Provider = "kimi"
|
||||
ProviderZhipu Provider = "zhipu"
|
||||
ProviderDeepseek Provider = "deepseek"
|
||||
ProviderMinimax Provider = "minimax"
|
||||
ProviderOpencodeGo Provider = "opencode_go"
|
||||
)
|
||||
|
||||
func (pr Provider) String() string {
|
||||
@@ -117,7 +119,7 @@ func (pr Provider) String() string {
|
||||
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
|
||||
func ProviderValidator(pr Provider) error {
|
||||
switch pr {
|
||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek:
|
||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek, ProviderMinimax, ProviderOpencodeGo:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
|
||||
|
||||
+17
-1
@@ -4595,6 +4595,22 @@ func (c *ProxyClient) QueryAccounts(_m *Proxy) *AccountQuery {
|
||||
return query
|
||||
}
|
||||
|
||||
// QueryPrimaryProxies queries the primary_proxies edge of a Proxy.
|
||||
func (c *ProxyClient) QueryPrimaryProxies(_m *Proxy) *ProxyQuery {
|
||||
query := (&ProxyClient{config: c.config}).Query()
|
||||
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
|
||||
id := _m.ID
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(proxy.Table, proxy.FieldID, id),
|
||||
sqlgraph.To(proxy.Table, proxy.FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2M, true, proxy.PrimaryProxiesTable, proxy.PrimaryProxiesColumn),
|
||||
)
|
||||
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
|
||||
return fromV, nil
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// QueryBackupProxy queries the backup_proxy edge of a Proxy.
|
||||
func (c *ProxyClient) QueryBackupProxy(_m *Proxy) *ProxyQuery {
|
||||
query := (&ProxyClient{config: c.config}).Query()
|
||||
@@ -4603,7 +4619,7 @@ func (c *ProxyClient) QueryBackupProxy(_m *Proxy) *ProxyQuery {
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(proxy.Table, proxy.FieldID, id),
|
||||
sqlgraph.To(proxy.Table, proxy.FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
|
||||
)
|
||||
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
|
||||
return fromV, nil
|
||||
|
||||
+58
-12
@@ -122,6 +122,10 @@ type Group struct {
|
||||
AllowMessagesDispatch bool `json:"allow_messages_dispatch,omitempty"`
|
||||
// 是否允许此 OpenAI 分组访问 Live 接口
|
||||
AllowLive bool `json:"allow_live,omitempty"`
|
||||
// 是否强制此 OpenAI/Composite 分组请求使用 service_tier=priority
|
||||
ForceOpenaiFast bool `json:"force_openai_fast,omitempty"`
|
||||
// 是否让此 OpenAI/Composite 分组的 Fast 请求按 Standard 价格计费
|
||||
FreeOpenaiFast bool `json:"free_openai_fast,omitempty"`
|
||||
// 仅允许非 apikey 类型账号关联到此分组
|
||||
RequireOauthOnly bool `json:"require_oauth_only,omitempty"`
|
||||
// 调度时仅允许 privacy 已成功设置的账号
|
||||
@@ -130,13 +134,17 @@ type Group struct {
|
||||
DefaultMappedModel string `json:"default_mapped_model,omitempty"`
|
||||
// OpenAI Messages 调度模型配置:按 Claude 系列/精确模型映射到目标 GPT 模型
|
||||
MessagesDispatchModelConfig domain.OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config,omitempty"`
|
||||
// 自定义 /v1/models 展示列表配置;仅影响模型列表响应,不影响调度
|
||||
ModelsListConfig domain.GroupModelsListConfig `json:"models_list_config,omitempty"`
|
||||
// 分组模型白名单:同时约束模型列表接口与请求准入
|
||||
ModelAllowlist domain.GroupModelAllowlist `json:"model_allowlist,omitempty"`
|
||||
// 固定账号获取 Codex Model Manifest 配置;开启后 /models 请求只用选定账号拉取(仅 openai 平台)
|
||||
CodexModelsManifestConfig domain.GroupCodexModelsManifestConfig `json:"codex_models_manifest_config,omitempty"`
|
||||
// 分组 RPM 上限,0 表示不限制;设置后接管该分组用户的限流
|
||||
RpmLimit int `json:"rpm_limit,omitempty"`
|
||||
// OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max
|
||||
MaxReasoningEffort string `json:"max_reasoning_effort,omitempty"`
|
||||
// OpenAI reasoning effort 自定义精确映射;先映射再应用上限
|
||||
// 超过推理强度上限时的访问控制:downgrade 自动降档,deny 拒绝访问
|
||||
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit,omitempty"`
|
||||
// OpenAI reasoning effort 自定义映射;可按模型精确名、前缀或后缀限定,先映射再应用上限
|
||||
ReasoningEffortMappings []domain.ReasoningEffortMapping `json:"reasoning_effort_mappings,omitempty"`
|
||||
// 是否启用利润控制:调度时仅允许账号计费倍率满足毛利率要求的账号进入候选池
|
||||
ProfitControlEnabled bool `json:"profit_control_enabled,omitempty"`
|
||||
@@ -250,15 +258,15 @@ func (*Group) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case group.FieldVideoModelPrices, group.FieldModelPricing, group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldReasoningEffortMappings:
|
||||
case group.FieldVideoModelPrices, group.FieldModelPricing, group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelAllowlist, group.FieldCodexModelsManifestConfig, group.FieldReasoningEffortMappings:
|
||||
values[i] = new([]byte)
|
||||
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldLongContextPricingEnabled, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled:
|
||||
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldLongContextPricingEnabled, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldForceOpenaiFast, group.FieldFreeOpenaiFast, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled:
|
||||
values[i] = new(sql.NullBool)
|
||||
case group.FieldRateMultiplier, group.FieldPeakRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImageRateMultiplier, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, group.FieldBatchImageDiscountMultiplier, group.FieldBatchImageHoldMultiplier, group.FieldVideoRateMultiplier, group.FieldVideoPrice480p, group.FieldVideoPrice720p, group.FieldVideoPrice1080p, group.FieldWebSearchPricePerCall, group.FieldSearchPricePer1k, group.FieldAudioRealtimePricePerMin, group.FieldAudioTtsPricePerMillionChars, group.FieldAudioSttPricePerHour, group.FieldProfitMinMargin, group.FieldProfitSafetyBuffer:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldRpmLimit:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel, group.FieldMaxReasoningEffort:
|
||||
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel, group.FieldMaxReasoningEffort, group.FieldMaxReasoningEffortOverLimit:
|
||||
values[i] = new(sql.NullString)
|
||||
case group.FieldCreatedAt, group.FieldUpdatedAt, group.FieldDeletedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
@@ -616,6 +624,18 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.AllowLive = value.Bool
|
||||
}
|
||||
case group.FieldForceOpenaiFast:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field force_openai_fast", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ForceOpenaiFast = value.Bool
|
||||
}
|
||||
case group.FieldFreeOpenaiFast:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field free_openai_fast", values[i])
|
||||
} else if value.Valid {
|
||||
_m.FreeOpenaiFast = value.Bool
|
||||
}
|
||||
case group.FieldRequireOauthOnly:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field require_oauth_only", values[i])
|
||||
@@ -642,12 +662,20 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
return fmt.Errorf("unmarshal field messages_dispatch_model_config: %w", err)
|
||||
}
|
||||
}
|
||||
case group.FieldModelsListConfig:
|
||||
case group.FieldModelAllowlist:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field models_list_config", values[i])
|
||||
return fmt.Errorf("unexpected type %T for field model_allowlist", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &_m.ModelsListConfig); err != nil {
|
||||
return fmt.Errorf("unmarshal field models_list_config: %w", err)
|
||||
if err := json.Unmarshal(*value, &_m.ModelAllowlist); err != nil {
|
||||
return fmt.Errorf("unmarshal field model_allowlist: %w", err)
|
||||
}
|
||||
}
|
||||
case group.FieldCodexModelsManifestConfig:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field codex_models_manifest_config", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &_m.CodexModelsManifestConfig); err != nil {
|
||||
return fmt.Errorf("unmarshal field codex_models_manifest_config: %w", err)
|
||||
}
|
||||
}
|
||||
case group.FieldRpmLimit:
|
||||
@@ -662,6 +690,12 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.MaxReasoningEffort = value.String
|
||||
}
|
||||
case group.FieldMaxReasoningEffortOverLimit:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field max_reasoning_effort_over_limit", values[i])
|
||||
} else if value.Valid {
|
||||
_m.MaxReasoningEffortOverLimit = value.String
|
||||
}
|
||||
case group.FieldReasoningEffortMappings:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field reasoning_effort_mappings", values[i])
|
||||
@@ -955,6 +989,12 @@ func (_m *Group) String() string {
|
||||
builder.WriteString("allow_live=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.AllowLive))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("force_openai_fast=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ForceOpenaiFast))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("free_openai_fast=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.FreeOpenaiFast))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("require_oauth_only=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.RequireOauthOnly))
|
||||
builder.WriteString(", ")
|
||||
@@ -967,8 +1007,11 @@ func (_m *Group) String() string {
|
||||
builder.WriteString("messages_dispatch_model_config=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.MessagesDispatchModelConfig))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("models_list_config=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ModelsListConfig))
|
||||
builder.WriteString("model_allowlist=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ModelAllowlist))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("codex_models_manifest_config=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.CodexModelsManifestConfig))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("rpm_limit=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.RpmLimit))
|
||||
@@ -976,6 +1019,9 @@ func (_m *Group) String() string {
|
||||
builder.WriteString("max_reasoning_effort=")
|
||||
builder.WriteString(_m.MaxReasoningEffort)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("max_reasoning_effort_over_limit=")
|
||||
builder.WriteString(_m.MaxReasoningEffortOverLimit)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("reasoning_effort_mappings=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ReasoningEffortMappings))
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -118,6 +118,10 @@ const (
|
||||
FieldAllowMessagesDispatch = "allow_messages_dispatch"
|
||||
// FieldAllowLive holds the string denoting the allow_live field in the database.
|
||||
FieldAllowLive = "allow_live"
|
||||
// FieldForceOpenaiFast holds the string denoting the force_openai_fast field in the database.
|
||||
FieldForceOpenaiFast = "force_openai_fast"
|
||||
// FieldFreeOpenaiFast holds the string denoting the free_openai_fast field in the database.
|
||||
FieldFreeOpenaiFast = "free_openai_fast"
|
||||
// FieldRequireOauthOnly holds the string denoting the require_oauth_only field in the database.
|
||||
FieldRequireOauthOnly = "require_oauth_only"
|
||||
// FieldRequirePrivacySet holds the string denoting the require_privacy_set field in the database.
|
||||
@@ -126,12 +130,16 @@ const (
|
||||
FieldDefaultMappedModel = "default_mapped_model"
|
||||
// FieldMessagesDispatchModelConfig holds the string denoting the messages_dispatch_model_config field in the database.
|
||||
FieldMessagesDispatchModelConfig = "messages_dispatch_model_config"
|
||||
// FieldModelsListConfig holds the string denoting the models_list_config field in the database.
|
||||
FieldModelsListConfig = "models_list_config"
|
||||
// FieldModelAllowlist holds the string denoting the model_allowlist field in the database.
|
||||
FieldModelAllowlist = "model_allowlist"
|
||||
// FieldCodexModelsManifestConfig holds the string denoting the codex_models_manifest_config field in the database.
|
||||
FieldCodexModelsManifestConfig = "codex_models_manifest_config"
|
||||
// FieldRpmLimit holds the string denoting the rpm_limit field in the database.
|
||||
FieldRpmLimit = "rpm_limit"
|
||||
// FieldMaxReasoningEffort holds the string denoting the max_reasoning_effort field in the database.
|
||||
FieldMaxReasoningEffort = "max_reasoning_effort"
|
||||
// FieldMaxReasoningEffortOverLimit holds the string denoting the max_reasoning_effort_over_limit field in the database.
|
||||
FieldMaxReasoningEffortOverLimit = "max_reasoning_effort_over_limit"
|
||||
// FieldReasoningEffortMappings holds the string denoting the reasoning_effort_mappings field in the database.
|
||||
FieldReasoningEffortMappings = "reasoning_effort_mappings"
|
||||
// FieldProfitControlEnabled holds the string denoting the profit_control_enabled field in the database.
|
||||
@@ -266,13 +274,17 @@ var Columns = []string{
|
||||
FieldSortOrder,
|
||||
FieldAllowMessagesDispatch,
|
||||
FieldAllowLive,
|
||||
FieldForceOpenaiFast,
|
||||
FieldFreeOpenaiFast,
|
||||
FieldRequireOauthOnly,
|
||||
FieldRequirePrivacySet,
|
||||
FieldDefaultMappedModel,
|
||||
FieldMessagesDispatchModelConfig,
|
||||
FieldModelsListConfig,
|
||||
FieldModelAllowlist,
|
||||
FieldCodexModelsManifestConfig,
|
||||
FieldRpmLimit,
|
||||
FieldMaxReasoningEffort,
|
||||
FieldMaxReasoningEffortOverLimit,
|
||||
FieldReasoningEffortMappings,
|
||||
FieldProfitControlEnabled,
|
||||
FieldProfitMinMargin,
|
||||
@@ -386,6 +398,10 @@ var (
|
||||
DefaultAllowMessagesDispatch bool
|
||||
// DefaultAllowLive holds the default value on creation for the "allow_live" field.
|
||||
DefaultAllowLive bool
|
||||
// DefaultForceOpenaiFast holds the default value on creation for the "force_openai_fast" field.
|
||||
DefaultForceOpenaiFast bool
|
||||
// DefaultFreeOpenaiFast holds the default value on creation for the "free_openai_fast" field.
|
||||
DefaultFreeOpenaiFast bool
|
||||
// DefaultRequireOauthOnly holds the default value on creation for the "require_oauth_only" field.
|
||||
DefaultRequireOauthOnly bool
|
||||
// DefaultRequirePrivacySet holds the default value on creation for the "require_privacy_set" field.
|
||||
@@ -396,14 +412,20 @@ var (
|
||||
DefaultMappedModelValidator func(string) error
|
||||
// DefaultMessagesDispatchModelConfig holds the default value on creation for the "messages_dispatch_model_config" field.
|
||||
DefaultMessagesDispatchModelConfig domain.OpenAIMessagesDispatchModelConfig
|
||||
// DefaultModelsListConfig holds the default value on creation for the "models_list_config" field.
|
||||
DefaultModelsListConfig domain.GroupModelsListConfig
|
||||
// DefaultModelAllowlist holds the default value on creation for the "model_allowlist" field.
|
||||
DefaultModelAllowlist domain.GroupModelAllowlist
|
||||
// DefaultCodexModelsManifestConfig holds the default value on creation for the "codex_models_manifest_config" field.
|
||||
DefaultCodexModelsManifestConfig domain.GroupCodexModelsManifestConfig
|
||||
// DefaultRpmLimit holds the default value on creation for the "rpm_limit" field.
|
||||
DefaultRpmLimit int
|
||||
// DefaultMaxReasoningEffort holds the default value on creation for the "max_reasoning_effort" field.
|
||||
DefaultMaxReasoningEffort string
|
||||
// MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
|
||||
MaxReasoningEffortValidator func(string) error
|
||||
// DefaultMaxReasoningEffortOverLimit holds the default value on creation for the "max_reasoning_effort_over_limit" field.
|
||||
DefaultMaxReasoningEffortOverLimit string
|
||||
// MaxReasoningEffortOverLimitValidator is a validator for the "max_reasoning_effort_over_limit" field. It is called by the builders before save.
|
||||
MaxReasoningEffortOverLimitValidator func(string) error
|
||||
// DefaultReasoningEffortMappings holds the default value on creation for the "reasoning_effort_mappings" field.
|
||||
DefaultReasoningEffortMappings []domain.ReasoningEffortMapping
|
||||
// DefaultProfitControlEnabled holds the default value on creation for the "profit_control_enabled" field.
|
||||
@@ -657,6 +679,16 @@ func ByAllowLive(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAllowLive, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByForceOpenaiFast orders the results by the force_openai_fast field.
|
||||
func ByForceOpenaiFast(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldForceOpenaiFast, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByFreeOpenaiFast orders the results by the free_openai_fast field.
|
||||
func ByFreeOpenaiFast(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldFreeOpenaiFast, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByRequireOauthOnly orders the results by the require_oauth_only field.
|
||||
func ByRequireOauthOnly(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldRequireOauthOnly, opts...).ToFunc()
|
||||
@@ -682,6 +714,11 @@ func ByMaxReasoningEffort(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldMaxReasoningEffort, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByMaxReasoningEffortOverLimit orders the results by the max_reasoning_effort_over_limit field.
|
||||
func ByMaxReasoningEffortOverLimit(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldMaxReasoningEffortOverLimit, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByProfitControlEnabled orders the results by the profit_control_enabled field.
|
||||
func ByProfitControlEnabled(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldProfitControlEnabled, opts...).ToFunc()
|
||||
|
||||
@@ -290,6 +290,16 @@ func AllowLive(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAllowLive, v))
|
||||
}
|
||||
|
||||
// ForceOpenaiFast applies equality check predicate on the "force_openai_fast" field. It's identical to ForceOpenaiFastEQ.
|
||||
func ForceOpenaiFast(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldForceOpenaiFast, v))
|
||||
}
|
||||
|
||||
// FreeOpenaiFast applies equality check predicate on the "free_openai_fast" field. It's identical to FreeOpenaiFastEQ.
|
||||
func FreeOpenaiFast(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldFreeOpenaiFast, v))
|
||||
}
|
||||
|
||||
// RequireOauthOnly applies equality check predicate on the "require_oauth_only" field. It's identical to RequireOauthOnlyEQ.
|
||||
func RequireOauthOnly(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
|
||||
@@ -315,6 +325,11 @@ func MaxReasoningEffort(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimit applies equality check predicate on the "max_reasoning_effort_over_limit" field. It's identical to MaxReasoningEffortOverLimitEQ.
|
||||
func MaxReasoningEffortOverLimit(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// ProfitControlEnabled applies equality check predicate on the "profit_control_enabled" field. It's identical to ProfitControlEnabledEQ.
|
||||
func ProfitControlEnabled(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldProfitControlEnabled, v))
|
||||
@@ -2270,6 +2285,26 @@ func AllowLiveNEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldAllowLive, v))
|
||||
}
|
||||
|
||||
// ForceOpenaiFastEQ applies the EQ predicate on the "force_openai_fast" field.
|
||||
func ForceOpenaiFastEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldForceOpenaiFast, v))
|
||||
}
|
||||
|
||||
// ForceOpenaiFastNEQ applies the NEQ predicate on the "force_openai_fast" field.
|
||||
func ForceOpenaiFastNEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldForceOpenaiFast, v))
|
||||
}
|
||||
|
||||
// FreeOpenaiFastEQ applies the EQ predicate on the "free_openai_fast" field.
|
||||
func FreeOpenaiFastEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldFreeOpenaiFast, v))
|
||||
}
|
||||
|
||||
// FreeOpenaiFastNEQ applies the NEQ predicate on the "free_openai_fast" field.
|
||||
func FreeOpenaiFastNEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldFreeOpenaiFast, v))
|
||||
}
|
||||
|
||||
// RequireOauthOnlyEQ applies the EQ predicate on the "require_oauth_only" field.
|
||||
func RequireOauthOnlyEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
|
||||
@@ -2460,6 +2495,71 @@ func MaxReasoningEffortContainsFold(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldContainsFold(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitEQ applies the EQ predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitEQ(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitNEQ applies the NEQ predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitNEQ(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitIn applies the In predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitIn(vs ...string) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldMaxReasoningEffortOverLimit, vs...))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitNotIn applies the NotIn predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitNotIn(vs ...string) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldMaxReasoningEffortOverLimit, vs...))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitGT applies the GT predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitGT(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitGTE applies the GTE predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitGTE(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitLT applies the LT predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitLT(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitLTE applies the LTE predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitLTE(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitContains applies the Contains predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitContains(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldContains(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitHasPrefix applies the HasPrefix predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitHasPrefix(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldHasPrefix(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitHasSuffix applies the HasSuffix predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitHasSuffix(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldHasSuffix(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitEqualFold applies the EqualFold predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitEqualFold(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldEqualFold(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimitContainsFold applies the ContainsFold predicate on the "max_reasoning_effort_over_limit" field.
|
||||
func MaxReasoningEffortOverLimitContainsFold(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldContainsFold(FieldMaxReasoningEffortOverLimit, v))
|
||||
}
|
||||
|
||||
// ProfitControlEnabledEQ applies the EQ predicate on the "profit_control_enabled" field.
|
||||
func ProfitControlEnabledEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldProfitControlEnabled, v))
|
||||
|
||||
+297
-32
@@ -704,6 +704,34 @@ func (_c *GroupCreate) SetNillableAllowLive(v *bool) *GroupCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetForceOpenaiFast sets the "force_openai_fast" field.
|
||||
func (_c *GroupCreate) SetForceOpenaiFast(v bool) *GroupCreate {
|
||||
_c.mutation.SetForceOpenaiFast(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableForceOpenaiFast sets the "force_openai_fast" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableForceOpenaiFast(v *bool) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetForceOpenaiFast(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetFreeOpenaiFast sets the "free_openai_fast" field.
|
||||
func (_c *GroupCreate) SetFreeOpenaiFast(v bool) *GroupCreate {
|
||||
_c.mutation.SetFreeOpenaiFast(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableFreeOpenaiFast(v *bool) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetFreeOpenaiFast(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (_c *GroupCreate) SetRequireOauthOnly(v bool) *GroupCreate {
|
||||
_c.mutation.SetRequireOauthOnly(v)
|
||||
@@ -760,16 +788,30 @@ func (_c *GroupCreate) SetNillableMessagesDispatchModelConfig(v *domain.OpenAIMe
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetModelsListConfig sets the "models_list_config" field.
|
||||
func (_c *GroupCreate) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupCreate {
|
||||
_c.mutation.SetModelsListConfig(v)
|
||||
// SetModelAllowlist sets the "model_allowlist" field.
|
||||
func (_c *GroupCreate) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupCreate {
|
||||
_c.mutation.SetModelAllowlist(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableModelsListConfig sets the "models_list_config" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableModelsListConfig(v *domain.GroupModelsListConfig) *GroupCreate {
|
||||
// SetNillableModelAllowlist sets the "model_allowlist" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableModelAllowlist(v *domain.GroupModelAllowlist) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetModelsListConfig(*v)
|
||||
_c.SetModelAllowlist(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
|
||||
func (_c *GroupCreate) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupCreate {
|
||||
_c.mutation.SetCodexModelsManifestConfig(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableCodexModelsManifestConfig sets the "codex_models_manifest_config" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableCodexModelsManifestConfig(v *domain.GroupCodexModelsManifestConfig) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetCodexModelsManifestConfig(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
@@ -802,6 +844,20 @@ func (_c *GroupCreate) SetNillableMaxReasoningEffort(v *string) *GroupCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
|
||||
func (_c *GroupCreate) SetMaxReasoningEffortOverLimit(v string) *GroupCreate {
|
||||
_c.mutation.SetMaxReasoningEffortOverLimit(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetMaxReasoningEffortOverLimit(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (_c *GroupCreate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupCreate {
|
||||
_c.mutation.SetReasoningEffortMappings(v)
|
||||
@@ -1095,6 +1151,14 @@ func (_c *GroupCreate) defaults() error {
|
||||
v := group.DefaultAllowLive
|
||||
_c.mutation.SetAllowLive(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ForceOpenaiFast(); !ok {
|
||||
v := group.DefaultForceOpenaiFast
|
||||
_c.mutation.SetForceOpenaiFast(v)
|
||||
}
|
||||
if _, ok := _c.mutation.FreeOpenaiFast(); !ok {
|
||||
v := group.DefaultFreeOpenaiFast
|
||||
_c.mutation.SetFreeOpenaiFast(v)
|
||||
}
|
||||
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
|
||||
v := group.DefaultRequireOauthOnly
|
||||
_c.mutation.SetRequireOauthOnly(v)
|
||||
@@ -1111,9 +1175,13 @@ func (_c *GroupCreate) defaults() error {
|
||||
v := group.DefaultMessagesDispatchModelConfig
|
||||
_c.mutation.SetMessagesDispatchModelConfig(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ModelsListConfig(); !ok {
|
||||
v := group.DefaultModelsListConfig
|
||||
_c.mutation.SetModelsListConfig(v)
|
||||
if _, ok := _c.mutation.ModelAllowlist(); !ok {
|
||||
v := group.DefaultModelAllowlist
|
||||
_c.mutation.SetModelAllowlist(v)
|
||||
}
|
||||
if _, ok := _c.mutation.CodexModelsManifestConfig(); !ok {
|
||||
v := group.DefaultCodexModelsManifestConfig
|
||||
_c.mutation.SetCodexModelsManifestConfig(v)
|
||||
}
|
||||
if _, ok := _c.mutation.RpmLimit(); !ok {
|
||||
v := group.DefaultRpmLimit
|
||||
@@ -1123,6 +1191,10 @@ func (_c *GroupCreate) defaults() error {
|
||||
v := group.DefaultMaxReasoningEffort
|
||||
_c.mutation.SetMaxReasoningEffort(v)
|
||||
}
|
||||
if _, ok := _c.mutation.MaxReasoningEffortOverLimit(); !ok {
|
||||
v := group.DefaultMaxReasoningEffortOverLimit
|
||||
_c.mutation.SetMaxReasoningEffortOverLimit(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
|
||||
v := group.DefaultReasoningEffortMappings
|
||||
_c.mutation.SetReasoningEffortMappings(v)
|
||||
@@ -1286,6 +1358,12 @@ func (_c *GroupCreate) check() error {
|
||||
if _, ok := _c.mutation.AllowLive(); !ok {
|
||||
return &ValidationError{Name: "allow_live", err: errors.New(`ent: missing required field "Group.allow_live"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.ForceOpenaiFast(); !ok {
|
||||
return &ValidationError{Name: "force_openai_fast", err: errors.New(`ent: missing required field "Group.force_openai_fast"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.FreeOpenaiFast(); !ok {
|
||||
return &ValidationError{Name: "free_openai_fast", err: errors.New(`ent: missing required field "Group.free_openai_fast"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
|
||||
return &ValidationError{Name: "require_oauth_only", err: errors.New(`ent: missing required field "Group.require_oauth_only"`)}
|
||||
}
|
||||
@@ -1303,8 +1381,11 @@ func (_c *GroupCreate) check() error {
|
||||
if _, ok := _c.mutation.MessagesDispatchModelConfig(); !ok {
|
||||
return &ValidationError{Name: "messages_dispatch_model_config", err: errors.New(`ent: missing required field "Group.messages_dispatch_model_config"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.ModelsListConfig(); !ok {
|
||||
return &ValidationError{Name: "models_list_config", err: errors.New(`ent: missing required field "Group.models_list_config"`)}
|
||||
if _, ok := _c.mutation.ModelAllowlist(); !ok {
|
||||
return &ValidationError{Name: "model_allowlist", err: errors.New(`ent: missing required field "Group.model_allowlist"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.CodexModelsManifestConfig(); !ok {
|
||||
return &ValidationError{Name: "codex_models_manifest_config", err: errors.New(`ent: missing required field "Group.codex_models_manifest_config"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.RpmLimit(); !ok {
|
||||
return &ValidationError{Name: "rpm_limit", err: errors.New(`ent: missing required field "Group.rpm_limit"`)}
|
||||
@@ -1317,6 +1398,14 @@ func (_c *GroupCreate) check() error {
|
||||
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.MaxReasoningEffortOverLimit(); !ok {
|
||||
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: errors.New(`ent: missing required field "Group.max_reasoning_effort_over_limit"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.MaxReasoningEffortOverLimit(); ok {
|
||||
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
|
||||
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
|
||||
return &ValidationError{Name: "reasoning_effort_mappings", err: errors.New(`ent: missing required field "Group.reasoning_effort_mappings"`)}
|
||||
}
|
||||
@@ -1560,6 +1649,14 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
|
||||
_node.AllowLive = value
|
||||
}
|
||||
if value, ok := _c.mutation.ForceOpenaiFast(); ok {
|
||||
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
|
||||
_node.ForceOpenaiFast = value
|
||||
}
|
||||
if value, ok := _c.mutation.FreeOpenaiFast(); ok {
|
||||
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
|
||||
_node.FreeOpenaiFast = value
|
||||
}
|
||||
if value, ok := _c.mutation.RequireOauthOnly(); ok {
|
||||
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
|
||||
_node.RequireOauthOnly = value
|
||||
@@ -1576,9 +1673,13 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldMessagesDispatchModelConfig, field.TypeJSON, value)
|
||||
_node.MessagesDispatchModelConfig = value
|
||||
}
|
||||
if value, ok := _c.mutation.ModelsListConfig(); ok {
|
||||
_spec.SetField(group.FieldModelsListConfig, field.TypeJSON, value)
|
||||
_node.ModelsListConfig = value
|
||||
if value, ok := _c.mutation.ModelAllowlist(); ok {
|
||||
_spec.SetField(group.FieldModelAllowlist, field.TypeJSON, value)
|
||||
_node.ModelAllowlist = value
|
||||
}
|
||||
if value, ok := _c.mutation.CodexModelsManifestConfig(); ok {
|
||||
_spec.SetField(group.FieldCodexModelsManifestConfig, field.TypeJSON, value)
|
||||
_node.CodexModelsManifestConfig = value
|
||||
}
|
||||
if value, ok := _c.mutation.RpmLimit(); ok {
|
||||
_spec.SetField(group.FieldRpmLimit, field.TypeInt, value)
|
||||
@@ -1588,6 +1689,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
|
||||
_node.MaxReasoningEffort = value
|
||||
}
|
||||
if value, ok := _c.mutation.MaxReasoningEffortOverLimit(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
|
||||
_node.MaxReasoningEffortOverLimit = value
|
||||
}
|
||||
if value, ok := _c.mutation.ReasoningEffortMappings(); ok {
|
||||
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
|
||||
_node.ReasoningEffortMappings = value
|
||||
@@ -2618,6 +2723,30 @@ func (u *GroupUpsert) UpdateAllowLive() *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetForceOpenaiFast sets the "force_openai_fast" field.
|
||||
func (u *GroupUpsert) SetForceOpenaiFast(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldForceOpenaiFast, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateForceOpenaiFast sets the "force_openai_fast" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateForceOpenaiFast() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldForceOpenaiFast)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetFreeOpenaiFast sets the "free_openai_fast" field.
|
||||
func (u *GroupUpsert) SetFreeOpenaiFast(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldFreeOpenaiFast, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateFreeOpenaiFast() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldFreeOpenaiFast)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (u *GroupUpsert) SetRequireOauthOnly(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldRequireOauthOnly, v)
|
||||
@@ -2666,15 +2795,27 @@ func (u *GroupUpsert) UpdateMessagesDispatchModelConfig() *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetModelsListConfig sets the "models_list_config" field.
|
||||
func (u *GroupUpsert) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpsert {
|
||||
u.Set(group.FieldModelsListConfig, v)
|
||||
// SetModelAllowlist sets the "model_allowlist" field.
|
||||
func (u *GroupUpsert) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpsert {
|
||||
u.Set(group.FieldModelAllowlist, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateModelsListConfig sets the "models_list_config" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateModelsListConfig() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldModelsListConfig)
|
||||
// UpdateModelAllowlist sets the "model_allowlist" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateModelAllowlist() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldModelAllowlist)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
|
||||
func (u *GroupUpsert) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpsert {
|
||||
u.Set(group.FieldCodexModelsManifestConfig, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateCodexModelsManifestConfig sets the "codex_models_manifest_config" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateCodexModelsManifestConfig() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldCodexModelsManifestConfig)
|
||||
return u
|
||||
}
|
||||
|
||||
@@ -2708,6 +2849,18 @@ func (u *GroupUpsert) UpdateMaxReasoningEffort() *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
|
||||
func (u *GroupUpsert) SetMaxReasoningEffortOverLimit(v string) *GroupUpsert {
|
||||
u.Set(group.FieldMaxReasoningEffortOverLimit, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateMaxReasoningEffortOverLimit() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldMaxReasoningEffortOverLimit)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (u *GroupUpsert) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsert {
|
||||
u.Set(group.FieldReasoningEffortMappings, v)
|
||||
@@ -3817,6 +3970,34 @@ func (u *GroupUpsertOne) UpdateAllowLive() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetForceOpenaiFast sets the "force_openai_fast" field.
|
||||
func (u *GroupUpsertOne) SetForceOpenaiFast(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetForceOpenaiFast(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateForceOpenaiFast sets the "force_openai_fast" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateForceOpenaiFast() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateForceOpenaiFast()
|
||||
})
|
||||
}
|
||||
|
||||
// SetFreeOpenaiFast sets the "free_openai_fast" field.
|
||||
func (u *GroupUpsertOne) SetFreeOpenaiFast(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetFreeOpenaiFast(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateFreeOpenaiFast() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateFreeOpenaiFast()
|
||||
})
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (u *GroupUpsertOne) SetRequireOauthOnly(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -3873,17 +4054,31 @@ func (u *GroupUpsertOne) UpdateMessagesDispatchModelConfig() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetModelsListConfig sets the "models_list_config" field.
|
||||
func (u *GroupUpsertOne) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpsertOne {
|
||||
// SetModelAllowlist sets the "model_allowlist" field.
|
||||
func (u *GroupUpsertOne) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetModelsListConfig(v)
|
||||
s.SetModelAllowlist(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateModelsListConfig sets the "models_list_config" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateModelsListConfig() *GroupUpsertOne {
|
||||
// UpdateModelAllowlist sets the "model_allowlist" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateModelAllowlist() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateModelsListConfig()
|
||||
s.UpdateModelAllowlist()
|
||||
})
|
||||
}
|
||||
|
||||
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
|
||||
func (u *GroupUpsertOne) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetCodexModelsManifestConfig(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCodexModelsManifestConfig sets the "codex_models_manifest_config" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateCodexModelsManifestConfig() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateCodexModelsManifestConfig()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3922,6 +4117,20 @@ func (u *GroupUpsertOne) UpdateMaxReasoningEffort() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
|
||||
func (u *GroupUpsertOne) SetMaxReasoningEffortOverLimit(v string) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetMaxReasoningEffortOverLimit(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateMaxReasoningEffortOverLimit() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateMaxReasoningEffortOverLimit()
|
||||
})
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (u *GroupUpsertOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -5207,6 +5416,34 @@ func (u *GroupUpsertBulk) UpdateAllowLive() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetForceOpenaiFast sets the "force_openai_fast" field.
|
||||
func (u *GroupUpsertBulk) SetForceOpenaiFast(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetForceOpenaiFast(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateForceOpenaiFast sets the "force_openai_fast" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateForceOpenaiFast() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateForceOpenaiFast()
|
||||
})
|
||||
}
|
||||
|
||||
// SetFreeOpenaiFast sets the "free_openai_fast" field.
|
||||
func (u *GroupUpsertBulk) SetFreeOpenaiFast(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetFreeOpenaiFast(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateFreeOpenaiFast() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateFreeOpenaiFast()
|
||||
})
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (u *GroupUpsertBulk) SetRequireOauthOnly(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -5263,17 +5500,31 @@ func (u *GroupUpsertBulk) UpdateMessagesDispatchModelConfig() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetModelsListConfig sets the "models_list_config" field.
|
||||
func (u *GroupUpsertBulk) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpsertBulk {
|
||||
// SetModelAllowlist sets the "model_allowlist" field.
|
||||
func (u *GroupUpsertBulk) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetModelsListConfig(v)
|
||||
s.SetModelAllowlist(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateModelsListConfig sets the "models_list_config" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateModelsListConfig() *GroupUpsertBulk {
|
||||
// UpdateModelAllowlist sets the "model_allowlist" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateModelAllowlist() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateModelsListConfig()
|
||||
s.UpdateModelAllowlist()
|
||||
})
|
||||
}
|
||||
|
||||
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
|
||||
func (u *GroupUpsertBulk) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetCodexModelsManifestConfig(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCodexModelsManifestConfig sets the "codex_models_manifest_config" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateCodexModelsManifestConfig() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateCodexModelsManifestConfig()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5312,6 +5563,20 @@ func (u *GroupUpsertBulk) UpdateMaxReasoningEffort() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
|
||||
func (u *GroupUpsertBulk) SetMaxReasoningEffortOverLimit(v string) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetMaxReasoningEffortOverLimit(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateMaxReasoningEffortOverLimit() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateMaxReasoningEffortOverLimit()
|
||||
})
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (u *GroupUpsertBulk) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
|
||||
+162
-16
@@ -989,6 +989,34 @@ func (_u *GroupUpdate) SetNillableAllowLive(v *bool) *GroupUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetForceOpenaiFast sets the "force_openai_fast" field.
|
||||
func (_u *GroupUpdate) SetForceOpenaiFast(v bool) *GroupUpdate {
|
||||
_u.mutation.SetForceOpenaiFast(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableForceOpenaiFast sets the "force_openai_fast" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableForceOpenaiFast(v *bool) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetForceOpenaiFast(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetFreeOpenaiFast sets the "free_openai_fast" field.
|
||||
func (_u *GroupUpdate) SetFreeOpenaiFast(v bool) *GroupUpdate {
|
||||
_u.mutation.SetFreeOpenaiFast(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableFreeOpenaiFast(v *bool) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetFreeOpenaiFast(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (_u *GroupUpdate) SetRequireOauthOnly(v bool) *GroupUpdate {
|
||||
_u.mutation.SetRequireOauthOnly(v)
|
||||
@@ -1045,16 +1073,30 @@ func (_u *GroupUpdate) SetNillableMessagesDispatchModelConfig(v *domain.OpenAIMe
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetModelsListConfig sets the "models_list_config" field.
|
||||
func (_u *GroupUpdate) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpdate {
|
||||
_u.mutation.SetModelsListConfig(v)
|
||||
// SetModelAllowlist sets the "model_allowlist" field.
|
||||
func (_u *GroupUpdate) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpdate {
|
||||
_u.mutation.SetModelAllowlist(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableModelsListConfig sets the "models_list_config" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableModelsListConfig(v *domain.GroupModelsListConfig) *GroupUpdate {
|
||||
// SetNillableModelAllowlist sets the "model_allowlist" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableModelAllowlist(v *domain.GroupModelAllowlist) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetModelsListConfig(*v)
|
||||
_u.SetModelAllowlist(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
|
||||
func (_u *GroupUpdate) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpdate {
|
||||
_u.mutation.SetCodexModelsManifestConfig(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableCodexModelsManifestConfig sets the "codex_models_manifest_config" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableCodexModelsManifestConfig(v *domain.GroupCodexModelsManifestConfig) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetCodexModelsManifestConfig(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
@@ -1094,6 +1136,20 @@ func (_u *GroupUpdate) SetNillableMaxReasoningEffort(v *string) *GroupUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
|
||||
func (_u *GroupUpdate) SetMaxReasoningEffortOverLimit(v string) *GroupUpdate {
|
||||
_u.mutation.SetMaxReasoningEffortOverLimit(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetMaxReasoningEffortOverLimit(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (_u *GroupUpdate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdate {
|
||||
_u.mutation.SetReasoningEffortMappings(v)
|
||||
@@ -1487,6 +1543,11 @@ func (_u *GroupUpdate) check() error {
|
||||
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
|
||||
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
|
||||
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1797,6 +1858,12 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.AllowLive(); ok {
|
||||
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ForceOpenaiFast(); ok {
|
||||
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.FreeOpenaiFast(); ok {
|
||||
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RequireOauthOnly(); ok {
|
||||
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
|
||||
}
|
||||
@@ -1809,8 +1876,11 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.MessagesDispatchModelConfig(); ok {
|
||||
_spec.SetField(group.FieldMessagesDispatchModelConfig, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ModelsListConfig(); ok {
|
||||
_spec.SetField(group.FieldModelsListConfig, field.TypeJSON, value)
|
||||
if value, ok := _u.mutation.ModelAllowlist(); ok {
|
||||
_spec.SetField(group.FieldModelAllowlist, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.CodexModelsManifestConfig(); ok {
|
||||
_spec.SetField(group.FieldCodexModelsManifestConfig, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RpmLimit(); ok {
|
||||
_spec.SetField(group.FieldRpmLimit, field.TypeInt, value)
|
||||
@@ -1821,6 +1891,9 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
|
||||
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
|
||||
}
|
||||
@@ -3110,6 +3183,34 @@ func (_u *GroupUpdateOne) SetNillableAllowLive(v *bool) *GroupUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetForceOpenaiFast sets the "force_openai_fast" field.
|
||||
func (_u *GroupUpdateOne) SetForceOpenaiFast(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetForceOpenaiFast(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableForceOpenaiFast sets the "force_openai_fast" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableForceOpenaiFast(v *bool) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetForceOpenaiFast(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetFreeOpenaiFast sets the "free_openai_fast" field.
|
||||
func (_u *GroupUpdateOne) SetFreeOpenaiFast(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetFreeOpenaiFast(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableFreeOpenaiFast(v *bool) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetFreeOpenaiFast(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (_u *GroupUpdateOne) SetRequireOauthOnly(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetRequireOauthOnly(v)
|
||||
@@ -3166,16 +3267,30 @@ func (_u *GroupUpdateOne) SetNillableMessagesDispatchModelConfig(v *domain.OpenA
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetModelsListConfig sets the "models_list_config" field.
|
||||
func (_u *GroupUpdateOne) SetModelsListConfig(v domain.GroupModelsListConfig) *GroupUpdateOne {
|
||||
_u.mutation.SetModelsListConfig(v)
|
||||
// SetModelAllowlist sets the "model_allowlist" field.
|
||||
func (_u *GroupUpdateOne) SetModelAllowlist(v domain.GroupModelAllowlist) *GroupUpdateOne {
|
||||
_u.mutation.SetModelAllowlist(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableModelsListConfig sets the "models_list_config" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableModelsListConfig(v *domain.GroupModelsListConfig) *GroupUpdateOne {
|
||||
// SetNillableModelAllowlist sets the "model_allowlist" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableModelAllowlist(v *domain.GroupModelAllowlist) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetModelsListConfig(*v)
|
||||
_u.SetModelAllowlist(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
|
||||
func (_u *GroupUpdateOne) SetCodexModelsManifestConfig(v domain.GroupCodexModelsManifestConfig) *GroupUpdateOne {
|
||||
_u.mutation.SetCodexModelsManifestConfig(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableCodexModelsManifestConfig sets the "codex_models_manifest_config" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableCodexModelsManifestConfig(v *domain.GroupCodexModelsManifestConfig) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetCodexModelsManifestConfig(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
@@ -3215,6 +3330,20 @@ func (_u *GroupUpdateOne) SetNillableMaxReasoningEffort(v *string) *GroupUpdateO
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
|
||||
func (_u *GroupUpdateOne) SetMaxReasoningEffortOverLimit(v string) *GroupUpdateOne {
|
||||
_u.mutation.SetMaxReasoningEffortOverLimit(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetMaxReasoningEffortOverLimit(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (_u *GroupUpdateOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdateOne {
|
||||
_u.mutation.SetReasoningEffortMappings(v)
|
||||
@@ -3621,6 +3750,11 @@ func (_u *GroupUpdateOne) check() error {
|
||||
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
|
||||
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
|
||||
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3948,6 +4082,12 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if value, ok := _u.mutation.AllowLive(); ok {
|
||||
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ForceOpenaiFast(); ok {
|
||||
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.FreeOpenaiFast(); ok {
|
||||
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RequireOauthOnly(); ok {
|
||||
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
|
||||
}
|
||||
@@ -3960,8 +4100,11 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if value, ok := _u.mutation.MessagesDispatchModelConfig(); ok {
|
||||
_spec.SetField(group.FieldMessagesDispatchModelConfig, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ModelsListConfig(); ok {
|
||||
_spec.SetField(group.FieldModelsListConfig, field.TypeJSON, value)
|
||||
if value, ok := _u.mutation.ModelAllowlist(); ok {
|
||||
_spec.SetField(group.FieldModelAllowlist, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.CodexModelsManifestConfig(); ok {
|
||||
_spec.SetField(group.FieldCodexModelsManifestConfig, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RpmLimit(); ok {
|
||||
_spec.SetField(group.FieldRpmLimit, field.TypeInt, value)
|
||||
@@ -3972,6 +4115,9 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
|
||||
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
|
||||
}
|
||||
|
||||
@@ -623,7 +623,7 @@ var (
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "name", Type: field.TypeString, Size: 100},
|
||||
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek"}},
|
||||
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek", "minimax", "opencode_go"}},
|
||||
{Name: "check_mode", Type: field.TypeString, Size: 32, Default: "probe"},
|
||||
{Name: "account_id", Type: field.TypeInt64, Nullable: true},
|
||||
{Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"},
|
||||
@@ -776,7 +776,7 @@ var (
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "name", Type: field.TypeString, Size: 100},
|
||||
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek"}},
|
||||
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek", "minimax", "opencode_go"}},
|
||||
{Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"},
|
||||
{Name: "description", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
|
||||
{Name: "extra_headers", Type: field.TypeJSON},
|
||||
@@ -954,13 +954,17 @@ var (
|
||||
{Name: "sort_order", Type: field.TypeInt, Default: 0},
|
||||
{Name: "allow_messages_dispatch", Type: field.TypeBool, Default: false},
|
||||
{Name: "allow_live", Type: field.TypeBool, Default: false},
|
||||
{Name: "force_openai_fast", Type: field.TypeBool, Default: false},
|
||||
{Name: "free_openai_fast", Type: field.TypeBool, Default: false},
|
||||
{Name: "require_oauth_only", Type: field.TypeBool, Default: false},
|
||||
{Name: "require_privacy_set", Type: field.TypeBool, Default: false},
|
||||
{Name: "default_mapped_model", Type: field.TypeString, Size: 100, Default: ""},
|
||||
{Name: "messages_dispatch_model_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "models_list_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "model_allowlist", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "codex_models_manifest_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "rpm_limit", Type: field.TypeInt, Default: 0},
|
||||
{Name: "max_reasoning_effort", Type: field.TypeString, Size: 20, Default: ""},
|
||||
{Name: "max_reasoning_effort_over_limit", Type: field.TypeString, Size: 20, Default: "downgrade"},
|
||||
{Name: "reasoning_effort_mappings", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "profit_control_enabled", Type: field.TypeBool, Default: false},
|
||||
{Name: "profit_min_margin", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||
@@ -1124,6 +1128,7 @@ var (
|
||||
{Name: "amount", Type: field.TypeFloat64, SchemaType: map[string]string{"postgres": "decimal(20,2)"}},
|
||||
{Name: "pay_amount", Type: field.TypeFloat64, SchemaType: map[string]string{"postgres": "decimal(20,2)"}},
|
||||
{Name: "fee_rate", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||
{Name: "bonus_amount", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,2)"}},
|
||||
{Name: "recharge_code", Type: field.TypeString, Size: 64},
|
||||
{Name: "out_trade_no", Type: field.TypeString, Size: 64, Default: ""},
|
||||
{Name: "payment_type", Type: field.TypeString, Size: 30},
|
||||
@@ -1166,7 +1171,7 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "payment_orders_users_payment_orders",
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[39]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[40]},
|
||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
@@ -1175,7 +1180,7 @@ var (
|
||||
{
|
||||
Name: "paymentorder_out_trade_no",
|
||||
Unique: true,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[8]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[9]},
|
||||
Annotation: &entsql.IndexAnnotation{
|
||||
Where: "out_trade_no <> ''",
|
||||
},
|
||||
@@ -1183,37 +1188,37 @@ var (
|
||||
{
|
||||
Name: "paymentorder_user_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[39]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[40]},
|
||||
},
|
||||
{
|
||||
Name: "paymentorder_status",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[21]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[22]},
|
||||
},
|
||||
{
|
||||
Name: "paymentorder_expires_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[29]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[30]},
|
||||
},
|
||||
{
|
||||
Name: "paymentorder_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[37]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[38]},
|
||||
},
|
||||
{
|
||||
Name: "paymentorder_paid_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[30]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[31]},
|
||||
},
|
||||
{
|
||||
Name: "paymentorder_payment_type_paid_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[9], PaymentOrdersColumns[30]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[10], PaymentOrdersColumns[31]},
|
||||
},
|
||||
{
|
||||
Name: "paymentorder_order_type",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[14]},
|
||||
Columns: []*schema.Column{PaymentOrdersColumns[15]},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1409,7 +1414,7 @@ var (
|
||||
{Name: "expires_at", Type: field.TypeTime, Nullable: true},
|
||||
{Name: "fallback_mode", Type: field.TypeString, Size: 20, Default: "none"},
|
||||
{Name: "expiry_warn_days", Type: field.TypeInt, Default: 7},
|
||||
{Name: "backup_proxy_id", Type: field.TypeInt64, Unique: true, Nullable: true},
|
||||
{Name: "backup_proxy_id", Type: field.TypeInt64, Nullable: true},
|
||||
}
|
||||
// ProxiesTable holds the schema information for the "proxies" table.
|
||||
ProxiesTable = &schema.Table{
|
||||
@@ -1801,6 +1806,7 @@ var (
|
||||
{Name: "signup_source", Type: field.TypeString, Default: "email"},
|
||||
{Name: "last_login_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "last_active_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "restrict_public_groups", Type: field.TypeBool, Default: false},
|
||||
{Name: "balance_notify_enabled", Type: field.TypeBool, Default: true},
|
||||
{Name: "balance_notify_threshold_type", Type: field.TypeString, Default: "fixed"},
|
||||
{Name: "balance_notify_threshold", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
|
||||
+500
-60
@@ -22158,14 +22158,18 @@ type GroupMutation struct {
|
||||
addsort_order *int
|
||||
allow_messages_dispatch *bool
|
||||
allow_live *bool
|
||||
force_openai_fast *bool
|
||||
free_openai_fast *bool
|
||||
require_oauth_only *bool
|
||||
require_privacy_set *bool
|
||||
default_mapped_model *string
|
||||
messages_dispatch_model_config *domain.OpenAIMessagesDispatchModelConfig
|
||||
models_list_config *domain.GroupModelsListConfig
|
||||
model_allowlist *domain.GroupModelAllowlist
|
||||
codex_models_manifest_config *domain.GroupCodexModelsManifestConfig
|
||||
rpm_limit *int
|
||||
addrpm_limit *int
|
||||
max_reasoning_effort *string
|
||||
max_reasoning_effort_over_limit *string
|
||||
reasoning_effort_mappings *[]domain.ReasoningEffortMapping
|
||||
appendreasoning_effort_mappings []domain.ReasoningEffortMapping
|
||||
profit_control_enabled *bool
|
||||
@@ -24944,6 +24948,78 @@ func (m *GroupMutation) ResetAllowLive() {
|
||||
m.allow_live = nil
|
||||
}
|
||||
|
||||
// SetForceOpenaiFast sets the "force_openai_fast" field.
|
||||
func (m *GroupMutation) SetForceOpenaiFast(b bool) {
|
||||
m.force_openai_fast = &b
|
||||
}
|
||||
|
||||
// ForceOpenaiFast returns the value of the "force_openai_fast" field in the mutation.
|
||||
func (m *GroupMutation) ForceOpenaiFast() (r bool, exists bool) {
|
||||
v := m.force_openai_fast
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldForceOpenaiFast returns the old "force_openai_fast" field's value of the Group entity.
|
||||
// If the Group object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *GroupMutation) OldForceOpenaiFast(ctx context.Context) (v bool, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldForceOpenaiFast is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldForceOpenaiFast requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldForceOpenaiFast: %w", err)
|
||||
}
|
||||
return oldValue.ForceOpenaiFast, nil
|
||||
}
|
||||
|
||||
// ResetForceOpenaiFast resets all changes to the "force_openai_fast" field.
|
||||
func (m *GroupMutation) ResetForceOpenaiFast() {
|
||||
m.force_openai_fast = nil
|
||||
}
|
||||
|
||||
// SetFreeOpenaiFast sets the "free_openai_fast" field.
|
||||
func (m *GroupMutation) SetFreeOpenaiFast(b bool) {
|
||||
m.free_openai_fast = &b
|
||||
}
|
||||
|
||||
// FreeOpenaiFast returns the value of the "free_openai_fast" field in the mutation.
|
||||
func (m *GroupMutation) FreeOpenaiFast() (r bool, exists bool) {
|
||||
v := m.free_openai_fast
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldFreeOpenaiFast returns the old "free_openai_fast" field's value of the Group entity.
|
||||
// If the Group object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *GroupMutation) OldFreeOpenaiFast(ctx context.Context) (v bool, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldFreeOpenaiFast is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldFreeOpenaiFast requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldFreeOpenaiFast: %w", err)
|
||||
}
|
||||
return oldValue.FreeOpenaiFast, nil
|
||||
}
|
||||
|
||||
// ResetFreeOpenaiFast resets all changes to the "free_openai_fast" field.
|
||||
func (m *GroupMutation) ResetFreeOpenaiFast() {
|
||||
m.free_openai_fast = nil
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (m *GroupMutation) SetRequireOauthOnly(b bool) {
|
||||
m.require_oauth_only = &b
|
||||
@@ -25088,40 +25164,76 @@ func (m *GroupMutation) ResetMessagesDispatchModelConfig() {
|
||||
m.messages_dispatch_model_config = nil
|
||||
}
|
||||
|
||||
// SetModelsListConfig sets the "models_list_config" field.
|
||||
func (m *GroupMutation) SetModelsListConfig(dmlc domain.GroupModelsListConfig) {
|
||||
m.models_list_config = &dmlc
|
||||
// SetModelAllowlist sets the "model_allowlist" field.
|
||||
func (m *GroupMutation) SetModelAllowlist(dma domain.GroupModelAllowlist) {
|
||||
m.model_allowlist = &dma
|
||||
}
|
||||
|
||||
// ModelsListConfig returns the value of the "models_list_config" field in the mutation.
|
||||
func (m *GroupMutation) ModelsListConfig() (r domain.GroupModelsListConfig, exists bool) {
|
||||
v := m.models_list_config
|
||||
// ModelAllowlist returns the value of the "model_allowlist" field in the mutation.
|
||||
func (m *GroupMutation) ModelAllowlist() (r domain.GroupModelAllowlist, exists bool) {
|
||||
v := m.model_allowlist
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldModelsListConfig returns the old "models_list_config" field's value of the Group entity.
|
||||
// OldModelAllowlist returns the old "model_allowlist" field's value of the Group entity.
|
||||
// If the Group object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *GroupMutation) OldModelsListConfig(ctx context.Context) (v domain.GroupModelsListConfig, err error) {
|
||||
func (m *GroupMutation) OldModelAllowlist(ctx context.Context) (v domain.GroupModelAllowlist, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldModelsListConfig is only allowed on UpdateOne operations")
|
||||
return v, errors.New("OldModelAllowlist is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldModelsListConfig requires an ID field in the mutation")
|
||||
return v, errors.New("OldModelAllowlist requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldModelsListConfig: %w", err)
|
||||
return v, fmt.Errorf("querying old value for OldModelAllowlist: %w", err)
|
||||
}
|
||||
return oldValue.ModelsListConfig, nil
|
||||
return oldValue.ModelAllowlist, nil
|
||||
}
|
||||
|
||||
// ResetModelsListConfig resets all changes to the "models_list_config" field.
|
||||
func (m *GroupMutation) ResetModelsListConfig() {
|
||||
m.models_list_config = nil
|
||||
// ResetModelAllowlist resets all changes to the "model_allowlist" field.
|
||||
func (m *GroupMutation) ResetModelAllowlist() {
|
||||
m.model_allowlist = nil
|
||||
}
|
||||
|
||||
// SetCodexModelsManifestConfig sets the "codex_models_manifest_config" field.
|
||||
func (m *GroupMutation) SetCodexModelsManifestConfig(dcmmc domain.GroupCodexModelsManifestConfig) {
|
||||
m.codex_models_manifest_config = &dcmmc
|
||||
}
|
||||
|
||||
// CodexModelsManifestConfig returns the value of the "codex_models_manifest_config" field in the mutation.
|
||||
func (m *GroupMutation) CodexModelsManifestConfig() (r domain.GroupCodexModelsManifestConfig, exists bool) {
|
||||
v := m.codex_models_manifest_config
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldCodexModelsManifestConfig returns the old "codex_models_manifest_config" field's value of the Group entity.
|
||||
// If the Group object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *GroupMutation) OldCodexModelsManifestConfig(ctx context.Context) (v domain.GroupCodexModelsManifestConfig, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldCodexModelsManifestConfig is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldCodexModelsManifestConfig requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldCodexModelsManifestConfig: %w", err)
|
||||
}
|
||||
return oldValue.CodexModelsManifestConfig, nil
|
||||
}
|
||||
|
||||
// ResetCodexModelsManifestConfig resets all changes to the "codex_models_manifest_config" field.
|
||||
func (m *GroupMutation) ResetCodexModelsManifestConfig() {
|
||||
m.codex_models_manifest_config = nil
|
||||
}
|
||||
|
||||
// SetRpmLimit sets the "rpm_limit" field.
|
||||
@@ -25216,6 +25328,42 @@ func (m *GroupMutation) ResetMaxReasoningEffort() {
|
||||
m.max_reasoning_effort = nil
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
|
||||
func (m *GroupMutation) SetMaxReasoningEffortOverLimit(s string) {
|
||||
m.max_reasoning_effort_over_limit = &s
|
||||
}
|
||||
|
||||
// MaxReasoningEffortOverLimit returns the value of the "max_reasoning_effort_over_limit" field in the mutation.
|
||||
func (m *GroupMutation) MaxReasoningEffortOverLimit() (r string, exists bool) {
|
||||
v := m.max_reasoning_effort_over_limit
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldMaxReasoningEffortOverLimit returns the old "max_reasoning_effort_over_limit" field's value of the Group entity.
|
||||
// If the Group object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *GroupMutation) OldMaxReasoningEffortOverLimit(ctx context.Context) (v string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldMaxReasoningEffortOverLimit is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldMaxReasoningEffortOverLimit requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldMaxReasoningEffortOverLimit: %w", err)
|
||||
}
|
||||
return oldValue.MaxReasoningEffortOverLimit, nil
|
||||
}
|
||||
|
||||
// ResetMaxReasoningEffortOverLimit resets all changes to the "max_reasoning_effort_over_limit" field.
|
||||
func (m *GroupMutation) ResetMaxReasoningEffortOverLimit() {
|
||||
m.max_reasoning_effort_over_limit = nil
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (m *GroupMutation) SetReasoningEffortMappings(dem []domain.ReasoningEffortMapping) {
|
||||
m.reasoning_effort_mappings = &dem
|
||||
@@ -25773,7 +25921,7 @@ func (m *GroupMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *GroupMutation) Fields() []string {
|
||||
fields := make([]string, 0, 62)
|
||||
fields := make([]string, 0, 66)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, group.FieldCreatedAt)
|
||||
}
|
||||
@@ -25927,6 +26075,12 @@ func (m *GroupMutation) Fields() []string {
|
||||
if m.allow_live != nil {
|
||||
fields = append(fields, group.FieldAllowLive)
|
||||
}
|
||||
if m.force_openai_fast != nil {
|
||||
fields = append(fields, group.FieldForceOpenaiFast)
|
||||
}
|
||||
if m.free_openai_fast != nil {
|
||||
fields = append(fields, group.FieldFreeOpenaiFast)
|
||||
}
|
||||
if m.require_oauth_only != nil {
|
||||
fields = append(fields, group.FieldRequireOauthOnly)
|
||||
}
|
||||
@@ -25939,8 +26093,11 @@ func (m *GroupMutation) Fields() []string {
|
||||
if m.messages_dispatch_model_config != nil {
|
||||
fields = append(fields, group.FieldMessagesDispatchModelConfig)
|
||||
}
|
||||
if m.models_list_config != nil {
|
||||
fields = append(fields, group.FieldModelsListConfig)
|
||||
if m.model_allowlist != nil {
|
||||
fields = append(fields, group.FieldModelAllowlist)
|
||||
}
|
||||
if m.codex_models_manifest_config != nil {
|
||||
fields = append(fields, group.FieldCodexModelsManifestConfig)
|
||||
}
|
||||
if m.rpm_limit != nil {
|
||||
fields = append(fields, group.FieldRpmLimit)
|
||||
@@ -25948,6 +26105,9 @@ func (m *GroupMutation) Fields() []string {
|
||||
if m.max_reasoning_effort != nil {
|
||||
fields = append(fields, group.FieldMaxReasoningEffort)
|
||||
}
|
||||
if m.max_reasoning_effort_over_limit != nil {
|
||||
fields = append(fields, group.FieldMaxReasoningEffortOverLimit)
|
||||
}
|
||||
if m.reasoning_effort_mappings != nil {
|
||||
fields = append(fields, group.FieldReasoningEffortMappings)
|
||||
}
|
||||
@@ -26070,6 +26230,10 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.AllowMessagesDispatch()
|
||||
case group.FieldAllowLive:
|
||||
return m.AllowLive()
|
||||
case group.FieldForceOpenaiFast:
|
||||
return m.ForceOpenaiFast()
|
||||
case group.FieldFreeOpenaiFast:
|
||||
return m.FreeOpenaiFast()
|
||||
case group.FieldRequireOauthOnly:
|
||||
return m.RequireOauthOnly()
|
||||
case group.FieldRequirePrivacySet:
|
||||
@@ -26078,12 +26242,16 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.DefaultMappedModel()
|
||||
case group.FieldMessagesDispatchModelConfig:
|
||||
return m.MessagesDispatchModelConfig()
|
||||
case group.FieldModelsListConfig:
|
||||
return m.ModelsListConfig()
|
||||
case group.FieldModelAllowlist:
|
||||
return m.ModelAllowlist()
|
||||
case group.FieldCodexModelsManifestConfig:
|
||||
return m.CodexModelsManifestConfig()
|
||||
case group.FieldRpmLimit:
|
||||
return m.RpmLimit()
|
||||
case group.FieldMaxReasoningEffort:
|
||||
return m.MaxReasoningEffort()
|
||||
case group.FieldMaxReasoningEffortOverLimit:
|
||||
return m.MaxReasoningEffortOverLimit()
|
||||
case group.FieldReasoningEffortMappings:
|
||||
return m.ReasoningEffortMappings()
|
||||
case group.FieldProfitControlEnabled:
|
||||
@@ -26203,6 +26371,10 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e
|
||||
return m.OldAllowMessagesDispatch(ctx)
|
||||
case group.FieldAllowLive:
|
||||
return m.OldAllowLive(ctx)
|
||||
case group.FieldForceOpenaiFast:
|
||||
return m.OldForceOpenaiFast(ctx)
|
||||
case group.FieldFreeOpenaiFast:
|
||||
return m.OldFreeOpenaiFast(ctx)
|
||||
case group.FieldRequireOauthOnly:
|
||||
return m.OldRequireOauthOnly(ctx)
|
||||
case group.FieldRequirePrivacySet:
|
||||
@@ -26211,12 +26383,16 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e
|
||||
return m.OldDefaultMappedModel(ctx)
|
||||
case group.FieldMessagesDispatchModelConfig:
|
||||
return m.OldMessagesDispatchModelConfig(ctx)
|
||||
case group.FieldModelsListConfig:
|
||||
return m.OldModelsListConfig(ctx)
|
||||
case group.FieldModelAllowlist:
|
||||
return m.OldModelAllowlist(ctx)
|
||||
case group.FieldCodexModelsManifestConfig:
|
||||
return m.OldCodexModelsManifestConfig(ctx)
|
||||
case group.FieldRpmLimit:
|
||||
return m.OldRpmLimit(ctx)
|
||||
case group.FieldMaxReasoningEffort:
|
||||
return m.OldMaxReasoningEffort(ctx)
|
||||
case group.FieldMaxReasoningEffortOverLimit:
|
||||
return m.OldMaxReasoningEffortOverLimit(ctx)
|
||||
case group.FieldReasoningEffortMappings:
|
||||
return m.OldReasoningEffortMappings(ctx)
|
||||
case group.FieldProfitControlEnabled:
|
||||
@@ -26591,6 +26767,20 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetAllowLive(v)
|
||||
return nil
|
||||
case group.FieldForceOpenaiFast:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetForceOpenaiFast(v)
|
||||
return nil
|
||||
case group.FieldFreeOpenaiFast:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetFreeOpenaiFast(v)
|
||||
return nil
|
||||
case group.FieldRequireOauthOnly:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
@@ -26619,12 +26809,19 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetMessagesDispatchModelConfig(v)
|
||||
return nil
|
||||
case group.FieldModelsListConfig:
|
||||
v, ok := value.(domain.GroupModelsListConfig)
|
||||
case group.FieldModelAllowlist:
|
||||
v, ok := value.(domain.GroupModelAllowlist)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetModelsListConfig(v)
|
||||
m.SetModelAllowlist(v)
|
||||
return nil
|
||||
case group.FieldCodexModelsManifestConfig:
|
||||
v, ok := value.(domain.GroupCodexModelsManifestConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetCodexModelsManifestConfig(v)
|
||||
return nil
|
||||
case group.FieldRpmLimit:
|
||||
v, ok := value.(int)
|
||||
@@ -26640,6 +26837,13 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetMaxReasoningEffort(v)
|
||||
return nil
|
||||
case group.FieldMaxReasoningEffortOverLimit:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetMaxReasoningEffortOverLimit(v)
|
||||
return nil
|
||||
case group.FieldReasoningEffortMappings:
|
||||
v, ok := value.([]domain.ReasoningEffortMapping)
|
||||
if !ok {
|
||||
@@ -27332,6 +27536,12 @@ func (m *GroupMutation) ResetField(name string) error {
|
||||
case group.FieldAllowLive:
|
||||
m.ResetAllowLive()
|
||||
return nil
|
||||
case group.FieldForceOpenaiFast:
|
||||
m.ResetForceOpenaiFast()
|
||||
return nil
|
||||
case group.FieldFreeOpenaiFast:
|
||||
m.ResetFreeOpenaiFast()
|
||||
return nil
|
||||
case group.FieldRequireOauthOnly:
|
||||
m.ResetRequireOauthOnly()
|
||||
return nil
|
||||
@@ -27344,8 +27554,11 @@ func (m *GroupMutation) ResetField(name string) error {
|
||||
case group.FieldMessagesDispatchModelConfig:
|
||||
m.ResetMessagesDispatchModelConfig()
|
||||
return nil
|
||||
case group.FieldModelsListConfig:
|
||||
m.ResetModelsListConfig()
|
||||
case group.FieldModelAllowlist:
|
||||
m.ResetModelAllowlist()
|
||||
return nil
|
||||
case group.FieldCodexModelsManifestConfig:
|
||||
m.ResetCodexModelsManifestConfig()
|
||||
return nil
|
||||
case group.FieldRpmLimit:
|
||||
m.ResetRpmLimit()
|
||||
@@ -27353,6 +27566,9 @@ func (m *GroupMutation) ResetField(name string) error {
|
||||
case group.FieldMaxReasoningEffort:
|
||||
m.ResetMaxReasoningEffort()
|
||||
return nil
|
||||
case group.FieldMaxReasoningEffortOverLimit:
|
||||
m.ResetMaxReasoningEffortOverLimit()
|
||||
return nil
|
||||
case group.FieldReasoningEffortMappings:
|
||||
m.ResetReasoningEffortMappings()
|
||||
return nil
|
||||
@@ -29897,6 +30113,8 @@ type PaymentOrderMutation struct {
|
||||
addpay_amount *float64
|
||||
fee_rate *float64
|
||||
addfee_rate *float64
|
||||
bonus_amount *float64
|
||||
addbonus_amount *float64
|
||||
recharge_code *string
|
||||
out_trade_no *string
|
||||
payment_type *string
|
||||
@@ -30364,6 +30582,62 @@ func (m *PaymentOrderMutation) ResetFeeRate() {
|
||||
m.addfee_rate = nil
|
||||
}
|
||||
|
||||
// SetBonusAmount sets the "bonus_amount" field.
|
||||
func (m *PaymentOrderMutation) SetBonusAmount(f float64) {
|
||||
m.bonus_amount = &f
|
||||
m.addbonus_amount = nil
|
||||
}
|
||||
|
||||
// BonusAmount returns the value of the "bonus_amount" field in the mutation.
|
||||
func (m *PaymentOrderMutation) BonusAmount() (r float64, exists bool) {
|
||||
v := m.bonus_amount
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldBonusAmount returns the old "bonus_amount" field's value of the PaymentOrder entity.
|
||||
// If the PaymentOrder object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *PaymentOrderMutation) OldBonusAmount(ctx context.Context) (v float64, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldBonusAmount is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldBonusAmount requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldBonusAmount: %w", err)
|
||||
}
|
||||
return oldValue.BonusAmount, nil
|
||||
}
|
||||
|
||||
// AddBonusAmount adds f to the "bonus_amount" field.
|
||||
func (m *PaymentOrderMutation) AddBonusAmount(f float64) {
|
||||
if m.addbonus_amount != nil {
|
||||
*m.addbonus_amount += f
|
||||
} else {
|
||||
m.addbonus_amount = &f
|
||||
}
|
||||
}
|
||||
|
||||
// AddedBonusAmount returns the value that was added to the "bonus_amount" field in this mutation.
|
||||
func (m *PaymentOrderMutation) AddedBonusAmount() (r float64, exists bool) {
|
||||
v := m.addbonus_amount
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ResetBonusAmount resets all changes to the "bonus_amount" field.
|
||||
func (m *PaymentOrderMutation) ResetBonusAmount() {
|
||||
m.bonus_amount = nil
|
||||
m.addbonus_amount = nil
|
||||
}
|
||||
|
||||
// SetRechargeCode sets the "recharge_code" field.
|
||||
func (m *PaymentOrderMutation) SetRechargeCode(s string) {
|
||||
m.recharge_code = &s
|
||||
@@ -31907,7 +32181,7 @@ func (m *PaymentOrderMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *PaymentOrderMutation) Fields() []string {
|
||||
fields := make([]string, 0, 39)
|
||||
fields := make([]string, 0, 40)
|
||||
if m.user != nil {
|
||||
fields = append(fields, paymentorder.FieldUserID)
|
||||
}
|
||||
@@ -31929,6 +32203,9 @@ func (m *PaymentOrderMutation) Fields() []string {
|
||||
if m.fee_rate != nil {
|
||||
fields = append(fields, paymentorder.FieldFeeRate)
|
||||
}
|
||||
if m.bonus_amount != nil {
|
||||
fields = append(fields, paymentorder.FieldBonusAmount)
|
||||
}
|
||||
if m.recharge_code != nil {
|
||||
fields = append(fields, paymentorder.FieldRechargeCode)
|
||||
}
|
||||
@@ -32047,6 +32324,8 @@ func (m *PaymentOrderMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.PayAmount()
|
||||
case paymentorder.FieldFeeRate:
|
||||
return m.FeeRate()
|
||||
case paymentorder.FieldBonusAmount:
|
||||
return m.BonusAmount()
|
||||
case paymentorder.FieldRechargeCode:
|
||||
return m.RechargeCode()
|
||||
case paymentorder.FieldOutTradeNo:
|
||||
@@ -32134,6 +32413,8 @@ func (m *PaymentOrderMutation) OldField(ctx context.Context, name string) (ent.V
|
||||
return m.OldPayAmount(ctx)
|
||||
case paymentorder.FieldFeeRate:
|
||||
return m.OldFeeRate(ctx)
|
||||
case paymentorder.FieldBonusAmount:
|
||||
return m.OldBonusAmount(ctx)
|
||||
case paymentorder.FieldRechargeCode:
|
||||
return m.OldRechargeCode(ctx)
|
||||
case paymentorder.FieldOutTradeNo:
|
||||
@@ -32256,6 +32537,13 @@ func (m *PaymentOrderMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetFeeRate(v)
|
||||
return nil
|
||||
case paymentorder.FieldBonusAmount:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetBonusAmount(v)
|
||||
return nil
|
||||
case paymentorder.FieldRechargeCode:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
@@ -32497,6 +32785,9 @@ func (m *PaymentOrderMutation) AddedFields() []string {
|
||||
if m.addfee_rate != nil {
|
||||
fields = append(fields, paymentorder.FieldFeeRate)
|
||||
}
|
||||
if m.addbonus_amount != nil {
|
||||
fields = append(fields, paymentorder.FieldBonusAmount)
|
||||
}
|
||||
if m.addplan_id != nil {
|
||||
fields = append(fields, paymentorder.FieldPlanID)
|
||||
}
|
||||
@@ -32523,6 +32814,8 @@ func (m *PaymentOrderMutation) AddedField(name string) (ent.Value, bool) {
|
||||
return m.AddedPayAmount()
|
||||
case paymentorder.FieldFeeRate:
|
||||
return m.AddedFeeRate()
|
||||
case paymentorder.FieldBonusAmount:
|
||||
return m.AddedBonusAmount()
|
||||
case paymentorder.FieldPlanID:
|
||||
return m.AddedPlanID()
|
||||
case paymentorder.FieldSubscriptionGroupID:
|
||||
@@ -32561,6 +32854,13 @@ func (m *PaymentOrderMutation) AddField(name string, value ent.Value) error {
|
||||
}
|
||||
m.AddFeeRate(v)
|
||||
return nil
|
||||
case paymentorder.FieldBonusAmount:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddBonusAmount(v)
|
||||
return nil
|
||||
case paymentorder.FieldPlanID:
|
||||
v, ok := value.(int64)
|
||||
if !ok {
|
||||
@@ -32760,6 +33060,9 @@ func (m *PaymentOrderMutation) ResetField(name string) error {
|
||||
case paymentorder.FieldFeeRate:
|
||||
m.ResetFeeRate()
|
||||
return nil
|
||||
case paymentorder.FieldBonusAmount:
|
||||
m.ResetBonusAmount()
|
||||
return nil
|
||||
case paymentorder.FieldRechargeCode:
|
||||
m.ResetRechargeCode()
|
||||
return nil
|
||||
@@ -37150,33 +37453,36 @@ func (m *PromoCodeUsageMutation) ResetEdge(name string) error {
|
||||
// ProxyMutation represents an operation that mutates the Proxy nodes in the graph.
|
||||
type ProxyMutation struct {
|
||||
config
|
||||
op Op
|
||||
typ string
|
||||
id *int64
|
||||
created_at *time.Time
|
||||
updated_at *time.Time
|
||||
deleted_at *time.Time
|
||||
name *string
|
||||
protocol *string
|
||||
host *string
|
||||
port *int
|
||||
addport *int
|
||||
username *string
|
||||
password *string
|
||||
status *string
|
||||
expires_at *time.Time
|
||||
fallback_mode *string
|
||||
expiry_warn_days *int
|
||||
addexpiry_warn_days *int
|
||||
clearedFields map[string]struct{}
|
||||
accounts map[int64]struct{}
|
||||
removedaccounts map[int64]struct{}
|
||||
clearedaccounts bool
|
||||
backup_proxy *int64
|
||||
clearedbackup_proxy bool
|
||||
done bool
|
||||
oldValue func(context.Context) (*Proxy, error)
|
||||
predicates []predicate.Proxy
|
||||
op Op
|
||||
typ string
|
||||
id *int64
|
||||
created_at *time.Time
|
||||
updated_at *time.Time
|
||||
deleted_at *time.Time
|
||||
name *string
|
||||
protocol *string
|
||||
host *string
|
||||
port *int
|
||||
addport *int
|
||||
username *string
|
||||
password *string
|
||||
status *string
|
||||
expires_at *time.Time
|
||||
fallback_mode *string
|
||||
expiry_warn_days *int
|
||||
addexpiry_warn_days *int
|
||||
clearedFields map[string]struct{}
|
||||
accounts map[int64]struct{}
|
||||
removedaccounts map[int64]struct{}
|
||||
clearedaccounts bool
|
||||
primary_proxies map[int64]struct{}
|
||||
removedprimary_proxies map[int64]struct{}
|
||||
clearedprimary_proxies bool
|
||||
backup_proxy *int64
|
||||
clearedbackup_proxy bool
|
||||
done bool
|
||||
oldValue func(context.Context) (*Proxy, error)
|
||||
predicates []predicate.Proxy
|
||||
}
|
||||
|
||||
var _ ent.Mutation = (*ProxyMutation)(nil)
|
||||
@@ -37940,6 +38246,60 @@ func (m *ProxyMutation) ResetAccounts() {
|
||||
m.removedaccounts = nil
|
||||
}
|
||||
|
||||
// AddPrimaryProxyIDs adds the "primary_proxies" edge to the Proxy entity by ids.
|
||||
func (m *ProxyMutation) AddPrimaryProxyIDs(ids ...int64) {
|
||||
if m.primary_proxies == nil {
|
||||
m.primary_proxies = make(map[int64]struct{})
|
||||
}
|
||||
for i := range ids {
|
||||
m.primary_proxies[ids[i]] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// ClearPrimaryProxies clears the "primary_proxies" edge to the Proxy entity.
|
||||
func (m *ProxyMutation) ClearPrimaryProxies() {
|
||||
m.clearedprimary_proxies = true
|
||||
}
|
||||
|
||||
// PrimaryProxiesCleared reports if the "primary_proxies" edge to the Proxy entity was cleared.
|
||||
func (m *ProxyMutation) PrimaryProxiesCleared() bool {
|
||||
return m.clearedprimary_proxies
|
||||
}
|
||||
|
||||
// RemovePrimaryProxyIDs removes the "primary_proxies" edge to the Proxy entity by IDs.
|
||||
func (m *ProxyMutation) RemovePrimaryProxyIDs(ids ...int64) {
|
||||
if m.removedprimary_proxies == nil {
|
||||
m.removedprimary_proxies = make(map[int64]struct{})
|
||||
}
|
||||
for i := range ids {
|
||||
delete(m.primary_proxies, ids[i])
|
||||
m.removedprimary_proxies[ids[i]] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// RemovedPrimaryProxies returns the removed IDs of the "primary_proxies" edge to the Proxy entity.
|
||||
func (m *ProxyMutation) RemovedPrimaryProxiesIDs() (ids []int64) {
|
||||
for id := range m.removedprimary_proxies {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PrimaryProxiesIDs returns the "primary_proxies" edge IDs in the mutation.
|
||||
func (m *ProxyMutation) PrimaryProxiesIDs() (ids []int64) {
|
||||
for id := range m.primary_proxies {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ResetPrimaryProxies resets all changes to the "primary_proxies" edge.
|
||||
func (m *ProxyMutation) ResetPrimaryProxies() {
|
||||
m.primary_proxies = nil
|
||||
m.clearedprimary_proxies = false
|
||||
m.removedprimary_proxies = nil
|
||||
}
|
||||
|
||||
// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity.
|
||||
func (m *ProxyMutation) ClearBackupProxy() {
|
||||
m.clearedbackup_proxy = true
|
||||
@@ -38381,10 +38741,13 @@ func (m *ProxyMutation) ResetField(name string) error {
|
||||
|
||||
// AddedEdges returns all edge names that were set/added in this mutation.
|
||||
func (m *ProxyMutation) AddedEdges() []string {
|
||||
edges := make([]string, 0, 2)
|
||||
edges := make([]string, 0, 3)
|
||||
if m.accounts != nil {
|
||||
edges = append(edges, proxy.EdgeAccounts)
|
||||
}
|
||||
if m.primary_proxies != nil {
|
||||
edges = append(edges, proxy.EdgePrimaryProxies)
|
||||
}
|
||||
if m.backup_proxy != nil {
|
||||
edges = append(edges, proxy.EdgeBackupProxy)
|
||||
}
|
||||
@@ -38401,6 +38764,12 @@ func (m *ProxyMutation) AddedIDs(name string) []ent.Value {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
case proxy.EdgePrimaryProxies:
|
||||
ids := make([]ent.Value, 0, len(m.primary_proxies))
|
||||
for id := range m.primary_proxies {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
case proxy.EdgeBackupProxy:
|
||||
if id := m.backup_proxy; id != nil {
|
||||
return []ent.Value{*id}
|
||||
@@ -38411,10 +38780,13 @@ func (m *ProxyMutation) AddedIDs(name string) []ent.Value {
|
||||
|
||||
// RemovedEdges returns all edge names that were removed in this mutation.
|
||||
func (m *ProxyMutation) RemovedEdges() []string {
|
||||
edges := make([]string, 0, 2)
|
||||
edges := make([]string, 0, 3)
|
||||
if m.removedaccounts != nil {
|
||||
edges = append(edges, proxy.EdgeAccounts)
|
||||
}
|
||||
if m.removedprimary_proxies != nil {
|
||||
edges = append(edges, proxy.EdgePrimaryProxies)
|
||||
}
|
||||
return edges
|
||||
}
|
||||
|
||||
@@ -38428,16 +38800,25 @@ func (m *ProxyMutation) RemovedIDs(name string) []ent.Value {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
case proxy.EdgePrimaryProxies:
|
||||
ids := make([]ent.Value, 0, len(m.removedprimary_proxies))
|
||||
for id := range m.removedprimary_proxies {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearedEdges returns all edge names that were cleared in this mutation.
|
||||
func (m *ProxyMutation) ClearedEdges() []string {
|
||||
edges := make([]string, 0, 2)
|
||||
edges := make([]string, 0, 3)
|
||||
if m.clearedaccounts {
|
||||
edges = append(edges, proxy.EdgeAccounts)
|
||||
}
|
||||
if m.clearedprimary_proxies {
|
||||
edges = append(edges, proxy.EdgePrimaryProxies)
|
||||
}
|
||||
if m.clearedbackup_proxy {
|
||||
edges = append(edges, proxy.EdgeBackupProxy)
|
||||
}
|
||||
@@ -38450,6 +38831,8 @@ func (m *ProxyMutation) EdgeCleared(name string) bool {
|
||||
switch name {
|
||||
case proxy.EdgeAccounts:
|
||||
return m.clearedaccounts
|
||||
case proxy.EdgePrimaryProxies:
|
||||
return m.clearedprimary_proxies
|
||||
case proxy.EdgeBackupProxy:
|
||||
return m.clearedbackup_proxy
|
||||
}
|
||||
@@ -38474,6 +38857,9 @@ func (m *ProxyMutation) ResetEdge(name string) error {
|
||||
case proxy.EdgeAccounts:
|
||||
m.ResetAccounts()
|
||||
return nil
|
||||
case proxy.EdgePrimaryProxies:
|
||||
m.ResetPrimaryProxies()
|
||||
return nil
|
||||
case proxy.EdgeBackupProxy:
|
||||
m.ResetBackupProxy()
|
||||
return nil
|
||||
@@ -48422,6 +48808,7 @@ type UserMutation struct {
|
||||
signup_source *string
|
||||
last_login_at *time.Time
|
||||
last_active_at *time.Time
|
||||
restrict_public_groups *bool
|
||||
balance_notify_enabled *bool
|
||||
balance_notify_threshold_type *string
|
||||
balance_notify_threshold *float64
|
||||
@@ -49347,6 +49734,42 @@ func (m *UserMutation) ResetLastActiveAt() {
|
||||
delete(m.clearedFields, user.FieldLastActiveAt)
|
||||
}
|
||||
|
||||
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
|
||||
func (m *UserMutation) SetRestrictPublicGroups(b bool) {
|
||||
m.restrict_public_groups = &b
|
||||
}
|
||||
|
||||
// RestrictPublicGroups returns the value of the "restrict_public_groups" field in the mutation.
|
||||
func (m *UserMutation) RestrictPublicGroups() (r bool, exists bool) {
|
||||
v := m.restrict_public_groups
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldRestrictPublicGroups returns the old "restrict_public_groups" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldRestrictPublicGroups(ctx context.Context) (v bool, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldRestrictPublicGroups is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldRestrictPublicGroups requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldRestrictPublicGroups: %w", err)
|
||||
}
|
||||
return oldValue.RestrictPublicGroups, nil
|
||||
}
|
||||
|
||||
// ResetRestrictPublicGroups resets all changes to the "restrict_public_groups" field.
|
||||
func (m *UserMutation) ResetRestrictPublicGroups() {
|
||||
m.restrict_public_groups = nil
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (m *UserMutation) SetBalanceNotifyEnabled(b bool) {
|
||||
m.balance_notify_enabled = &b
|
||||
@@ -50373,7 +50796,7 @@ func (m *UserMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *UserMutation) Fields() []string {
|
||||
fields := make([]string, 0, 24)
|
||||
fields := make([]string, 0, 25)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, user.FieldCreatedAt)
|
||||
}
|
||||
@@ -50428,6 +50851,9 @@ func (m *UserMutation) Fields() []string {
|
||||
if m.last_active_at != nil {
|
||||
fields = append(fields, user.FieldLastActiveAt)
|
||||
}
|
||||
if m.restrict_public_groups != nil {
|
||||
fields = append(fields, user.FieldRestrictPublicGroups)
|
||||
}
|
||||
if m.balance_notify_enabled != nil {
|
||||
fields = append(fields, user.FieldBalanceNotifyEnabled)
|
||||
}
|
||||
@@ -50490,6 +50916,8 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.LastLoginAt()
|
||||
case user.FieldLastActiveAt:
|
||||
return m.LastActiveAt()
|
||||
case user.FieldRestrictPublicGroups:
|
||||
return m.RestrictPublicGroups()
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
return m.BalanceNotifyEnabled()
|
||||
case user.FieldBalanceNotifyThresholdType:
|
||||
@@ -50547,6 +50975,8 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
|
||||
return m.OldLastLoginAt(ctx)
|
||||
case user.FieldLastActiveAt:
|
||||
return m.OldLastActiveAt(ctx)
|
||||
case user.FieldRestrictPublicGroups:
|
||||
return m.OldRestrictPublicGroups(ctx)
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
return m.OldBalanceNotifyEnabled(ctx)
|
||||
case user.FieldBalanceNotifyThresholdType:
|
||||
@@ -50694,6 +51124,13 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetLastActiveAt(v)
|
||||
return nil
|
||||
case user.FieldRestrictPublicGroups:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetRestrictPublicGroups(v)
|
||||
return nil
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
@@ -50953,6 +51390,9 @@ func (m *UserMutation) ResetField(name string) error {
|
||||
case user.FieldLastActiveAt:
|
||||
m.ResetLastActiveAt()
|
||||
return nil
|
||||
case user.FieldRestrictPublicGroups:
|
||||
m.ResetRestrictPublicGroups()
|
||||
return nil
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
m.ResetBalanceNotifyEnabled()
|
||||
return nil
|
||||
|
||||
@@ -33,6 +33,8 @@ type PaymentOrder struct {
|
||||
PayAmount float64 `json:"pay_amount,omitempty"`
|
||||
// FeeRate holds the value of the "fee_rate" field.
|
||||
FeeRate float64 `json:"fee_rate,omitempty"`
|
||||
// BonusAmount holds the value of the "bonus_amount" field.
|
||||
BonusAmount float64 `json:"bonus_amount,omitempty"`
|
||||
// RechargeCode holds the value of the "recharge_code" field.
|
||||
RechargeCode string `json:"recharge_code,omitempty"`
|
||||
// OutTradeNo holds the value of the "out_trade_no" field.
|
||||
@@ -132,7 +134,7 @@ func (*PaymentOrder) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new([]byte)
|
||||
case paymentorder.FieldForceRefund:
|
||||
values[i] = new(sql.NullBool)
|
||||
case paymentorder.FieldAmount, paymentorder.FieldPayAmount, paymentorder.FieldFeeRate, paymentorder.FieldRefundAmount:
|
||||
case paymentorder.FieldAmount, paymentorder.FieldPayAmount, paymentorder.FieldFeeRate, paymentorder.FieldBonusAmount, paymentorder.FieldRefundAmount:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
case paymentorder.FieldID, paymentorder.FieldUserID, paymentorder.FieldPlanID, paymentorder.FieldSubscriptionGroupID, paymentorder.FieldSubscriptionDays:
|
||||
values[i] = new(sql.NullInt64)
|
||||
@@ -204,6 +206,12 @@ func (_m *PaymentOrder) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.FeeRate = value.Float64
|
||||
}
|
||||
case paymentorder.FieldBonusAmount:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field bonus_amount", values[i])
|
||||
} else if value.Valid {
|
||||
_m.BonusAmount = value.Float64
|
||||
}
|
||||
case paymentorder.FieldRechargeCode:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field recharge_code", values[i])
|
||||
@@ -480,6 +488,9 @@ func (_m *PaymentOrder) String() string {
|
||||
builder.WriteString("fee_rate=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.FeeRate))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("bonus_amount=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.BonusAmount))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("recharge_code=")
|
||||
builder.WriteString(_m.RechargeCode)
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -28,6 +28,8 @@ const (
|
||||
FieldPayAmount = "pay_amount"
|
||||
// FieldFeeRate holds the string denoting the fee_rate field in the database.
|
||||
FieldFeeRate = "fee_rate"
|
||||
// FieldBonusAmount holds the string denoting the bonus_amount field in the database.
|
||||
FieldBonusAmount = "bonus_amount"
|
||||
// FieldRechargeCode holds the string denoting the recharge_code field in the database.
|
||||
FieldRechargeCode = "recharge_code"
|
||||
// FieldOutTradeNo holds the string denoting the out_trade_no field in the database.
|
||||
@@ -115,6 +117,7 @@ var Columns = []string{
|
||||
FieldAmount,
|
||||
FieldPayAmount,
|
||||
FieldFeeRate,
|
||||
FieldBonusAmount,
|
||||
FieldRechargeCode,
|
||||
FieldOutTradeNo,
|
||||
FieldPaymentType,
|
||||
@@ -166,6 +169,8 @@ var (
|
||||
UserNameValidator func(string) error
|
||||
// DefaultFeeRate holds the default value on creation for the "fee_rate" field.
|
||||
DefaultFeeRate float64
|
||||
// DefaultBonusAmount holds the default value on creation for the "bonus_amount" field.
|
||||
DefaultBonusAmount float64
|
||||
// RechargeCodeValidator is a validator for the "recharge_code" field. It is called by the builders before save.
|
||||
RechargeCodeValidator func(string) error
|
||||
// DefaultOutTradeNo holds the default value on creation for the "out_trade_no" field.
|
||||
@@ -249,6 +254,11 @@ func ByFeeRate(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldFeeRate, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByBonusAmount orders the results by the bonus_amount field.
|
||||
func ByBonusAmount(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldBonusAmount, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByRechargeCode orders the results by the recharge_code field.
|
||||
func ByRechargeCode(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldRechargeCode, opts...).ToFunc()
|
||||
|
||||
@@ -90,6 +90,11 @@ func FeeRate(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldEQ(FieldFeeRate, v))
|
||||
}
|
||||
|
||||
// BonusAmount applies equality check predicate on the "bonus_amount" field. It's identical to BonusAmountEQ.
|
||||
func BonusAmount(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldEQ(FieldBonusAmount, v))
|
||||
}
|
||||
|
||||
// RechargeCode applies equality check predicate on the "recharge_code" field. It's identical to RechargeCodeEQ.
|
||||
func RechargeCode(v string) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldEQ(FieldRechargeCode, v))
|
||||
@@ -590,6 +595,46 @@ func FeeRateLTE(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldLTE(FieldFeeRate, v))
|
||||
}
|
||||
|
||||
// BonusAmountEQ applies the EQ predicate on the "bonus_amount" field.
|
||||
func BonusAmountEQ(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldEQ(FieldBonusAmount, v))
|
||||
}
|
||||
|
||||
// BonusAmountNEQ applies the NEQ predicate on the "bonus_amount" field.
|
||||
func BonusAmountNEQ(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldNEQ(FieldBonusAmount, v))
|
||||
}
|
||||
|
||||
// BonusAmountIn applies the In predicate on the "bonus_amount" field.
|
||||
func BonusAmountIn(vs ...float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldIn(FieldBonusAmount, vs...))
|
||||
}
|
||||
|
||||
// BonusAmountNotIn applies the NotIn predicate on the "bonus_amount" field.
|
||||
func BonusAmountNotIn(vs ...float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldNotIn(FieldBonusAmount, vs...))
|
||||
}
|
||||
|
||||
// BonusAmountGT applies the GT predicate on the "bonus_amount" field.
|
||||
func BonusAmountGT(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldGT(FieldBonusAmount, v))
|
||||
}
|
||||
|
||||
// BonusAmountGTE applies the GTE predicate on the "bonus_amount" field.
|
||||
func BonusAmountGTE(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldGTE(FieldBonusAmount, v))
|
||||
}
|
||||
|
||||
// BonusAmountLT applies the LT predicate on the "bonus_amount" field.
|
||||
func BonusAmountLT(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldLT(FieldBonusAmount, v))
|
||||
}
|
||||
|
||||
// BonusAmountLTE applies the LTE predicate on the "bonus_amount" field.
|
||||
func BonusAmountLTE(v float64) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldLTE(FieldBonusAmount, v))
|
||||
}
|
||||
|
||||
// RechargeCodeEQ applies the EQ predicate on the "recharge_code" field.
|
||||
func RechargeCodeEQ(v string) predicate.PaymentOrder {
|
||||
return predicate.PaymentOrder(sql.FieldEQ(FieldRechargeCode, v))
|
||||
|
||||
@@ -81,6 +81,20 @@ func (_c *PaymentOrderCreate) SetNillableFeeRate(v *float64) *PaymentOrderCreate
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetBonusAmount sets the "bonus_amount" field.
|
||||
func (_c *PaymentOrderCreate) SetBonusAmount(v float64) *PaymentOrderCreate {
|
||||
_c.mutation.SetBonusAmount(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableBonusAmount sets the "bonus_amount" field if the given value is not nil.
|
||||
func (_c *PaymentOrderCreate) SetNillableBonusAmount(v *float64) *PaymentOrderCreate {
|
||||
if v != nil {
|
||||
_c.SetBonusAmount(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetRechargeCode sets the "recharge_code" field.
|
||||
func (_c *PaymentOrderCreate) SetRechargeCode(v string) *PaymentOrderCreate {
|
||||
_c.mutation.SetRechargeCode(v)
|
||||
@@ -517,6 +531,10 @@ func (_c *PaymentOrderCreate) defaults() {
|
||||
v := paymentorder.DefaultFeeRate
|
||||
_c.mutation.SetFeeRate(v)
|
||||
}
|
||||
if _, ok := _c.mutation.BonusAmount(); !ok {
|
||||
v := paymentorder.DefaultBonusAmount
|
||||
_c.mutation.SetBonusAmount(v)
|
||||
}
|
||||
if _, ok := _c.mutation.OutTradeNo(); !ok {
|
||||
v := paymentorder.DefaultOutTradeNo
|
||||
_c.mutation.SetOutTradeNo(v)
|
||||
@@ -577,6 +595,9 @@ func (_c *PaymentOrderCreate) check() error {
|
||||
if _, ok := _c.mutation.FeeRate(); !ok {
|
||||
return &ValidationError{Name: "fee_rate", err: errors.New(`ent: missing required field "PaymentOrder.fee_rate"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.BonusAmount(); !ok {
|
||||
return &ValidationError{Name: "bonus_amount", err: errors.New(`ent: missing required field "PaymentOrder.bonus_amount"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.RechargeCode(); !ok {
|
||||
return &ValidationError{Name: "recharge_code", err: errors.New(`ent: missing required field "PaymentOrder.recharge_code"`)}
|
||||
}
|
||||
@@ -725,6 +746,10 @@ func (_c *PaymentOrderCreate) createSpec() (*PaymentOrder, *sqlgraph.CreateSpec)
|
||||
_spec.SetField(paymentorder.FieldFeeRate, field.TypeFloat64, value)
|
||||
_node.FeeRate = value
|
||||
}
|
||||
if value, ok := _c.mutation.BonusAmount(); ok {
|
||||
_spec.SetField(paymentorder.FieldBonusAmount, field.TypeFloat64, value)
|
||||
_node.BonusAmount = value
|
||||
}
|
||||
if value, ok := _c.mutation.RechargeCode(); ok {
|
||||
_spec.SetField(paymentorder.FieldRechargeCode, field.TypeString, value)
|
||||
_node.RechargeCode = value
|
||||
@@ -1030,6 +1055,24 @@ func (u *PaymentOrderUpsert) AddFeeRate(v float64) *PaymentOrderUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetBonusAmount sets the "bonus_amount" field.
|
||||
func (u *PaymentOrderUpsert) SetBonusAmount(v float64) *PaymentOrderUpsert {
|
||||
u.Set(paymentorder.FieldBonusAmount, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateBonusAmount sets the "bonus_amount" field to the value that was provided on create.
|
||||
func (u *PaymentOrderUpsert) UpdateBonusAmount() *PaymentOrderUpsert {
|
||||
u.SetExcluded(paymentorder.FieldBonusAmount)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddBonusAmount adds v to the "bonus_amount" field.
|
||||
func (u *PaymentOrderUpsert) AddBonusAmount(v float64) *PaymentOrderUpsert {
|
||||
u.Add(paymentorder.FieldBonusAmount, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetRechargeCode sets the "recharge_code" field.
|
||||
func (u *PaymentOrderUpsert) SetRechargeCode(v string) *PaymentOrderUpsert {
|
||||
u.Set(paymentorder.FieldRechargeCode, v)
|
||||
@@ -1711,6 +1754,27 @@ func (u *PaymentOrderUpsertOne) UpdateFeeRate() *PaymentOrderUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetBonusAmount sets the "bonus_amount" field.
|
||||
func (u *PaymentOrderUpsertOne) SetBonusAmount(v float64) *PaymentOrderUpsertOne {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
s.SetBonusAmount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddBonusAmount adds v to the "bonus_amount" field.
|
||||
func (u *PaymentOrderUpsertOne) AddBonusAmount(v float64) *PaymentOrderUpsertOne {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
s.AddBonusAmount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateBonusAmount sets the "bonus_amount" field to the value that was provided on create.
|
||||
func (u *PaymentOrderUpsertOne) UpdateBonusAmount() *PaymentOrderUpsertOne {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
s.UpdateBonusAmount()
|
||||
})
|
||||
}
|
||||
|
||||
// SetRechargeCode sets the "recharge_code" field.
|
||||
func (u *PaymentOrderUpsertOne) SetRechargeCode(v string) *PaymentOrderUpsertOne {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
@@ -2643,6 +2707,27 @@ func (u *PaymentOrderUpsertBulk) UpdateFeeRate() *PaymentOrderUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetBonusAmount sets the "bonus_amount" field.
|
||||
func (u *PaymentOrderUpsertBulk) SetBonusAmount(v float64) *PaymentOrderUpsertBulk {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
s.SetBonusAmount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddBonusAmount adds v to the "bonus_amount" field.
|
||||
func (u *PaymentOrderUpsertBulk) AddBonusAmount(v float64) *PaymentOrderUpsertBulk {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
s.AddBonusAmount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateBonusAmount sets the "bonus_amount" field to the value that was provided on create.
|
||||
func (u *PaymentOrderUpsertBulk) UpdateBonusAmount() *PaymentOrderUpsertBulk {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
s.UpdateBonusAmount()
|
||||
})
|
||||
}
|
||||
|
||||
// SetRechargeCode sets the "recharge_code" field.
|
||||
func (u *PaymentOrderUpsertBulk) SetRechargeCode(v string) *PaymentOrderUpsertBulk {
|
||||
return u.Update(func(s *PaymentOrderUpsert) {
|
||||
|
||||
@@ -154,6 +154,27 @@ func (_u *PaymentOrderUpdate) AddFeeRate(v float64) *PaymentOrderUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetBonusAmount sets the "bonus_amount" field.
|
||||
func (_u *PaymentOrderUpdate) SetBonusAmount(v float64) *PaymentOrderUpdate {
|
||||
_u.mutation.ResetBonusAmount()
|
||||
_u.mutation.SetBonusAmount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableBonusAmount sets the "bonus_amount" field if the given value is not nil.
|
||||
func (_u *PaymentOrderUpdate) SetNillableBonusAmount(v *float64) *PaymentOrderUpdate {
|
||||
if v != nil {
|
||||
_u.SetBonusAmount(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddBonusAmount adds value to the "bonus_amount" field.
|
||||
func (_u *PaymentOrderUpdate) AddBonusAmount(v float64) *PaymentOrderUpdate {
|
||||
_u.mutation.AddBonusAmount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRechargeCode sets the "recharge_code" field.
|
||||
func (_u *PaymentOrderUpdate) SetRechargeCode(v string) *PaymentOrderUpdate {
|
||||
_u.mutation.SetRechargeCode(v)
|
||||
@@ -881,6 +902,12 @@ func (_u *PaymentOrderUpdate) sqlSave(ctx context.Context) (_node int, err error
|
||||
if value, ok := _u.mutation.AddedFeeRate(); ok {
|
||||
_spec.AddField(paymentorder.FieldFeeRate, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.BonusAmount(); ok {
|
||||
_spec.SetField(paymentorder.FieldBonusAmount, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedBonusAmount(); ok {
|
||||
_spec.AddField(paymentorder.FieldBonusAmount, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RechargeCode(); ok {
|
||||
_spec.SetField(paymentorder.FieldRechargeCode, field.TypeString, value)
|
||||
}
|
||||
@@ -1217,6 +1244,27 @@ func (_u *PaymentOrderUpdateOne) AddFeeRate(v float64) *PaymentOrderUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetBonusAmount sets the "bonus_amount" field.
|
||||
func (_u *PaymentOrderUpdateOne) SetBonusAmount(v float64) *PaymentOrderUpdateOne {
|
||||
_u.mutation.ResetBonusAmount()
|
||||
_u.mutation.SetBonusAmount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableBonusAmount sets the "bonus_amount" field if the given value is not nil.
|
||||
func (_u *PaymentOrderUpdateOne) SetNillableBonusAmount(v *float64) *PaymentOrderUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetBonusAmount(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddBonusAmount adds value to the "bonus_amount" field.
|
||||
func (_u *PaymentOrderUpdateOne) AddBonusAmount(v float64) *PaymentOrderUpdateOne {
|
||||
_u.mutation.AddBonusAmount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRechargeCode sets the "recharge_code" field.
|
||||
func (_u *PaymentOrderUpdateOne) SetRechargeCode(v string) *PaymentOrderUpdateOne {
|
||||
_u.mutation.SetRechargeCode(v)
|
||||
@@ -1974,6 +2022,12 @@ func (_u *PaymentOrderUpdateOne) sqlSave(ctx context.Context) (_node *PaymentOrd
|
||||
if value, ok := _u.mutation.AddedFeeRate(); ok {
|
||||
_spec.AddField(paymentorder.FieldFeeRate, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.BonusAmount(); ok {
|
||||
_spec.SetField(paymentorder.FieldBonusAmount, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedBonusAmount(); ok {
|
||||
_spec.AddField(paymentorder.FieldBonusAmount, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RechargeCode(); ok {
|
||||
_spec.SetField(paymentorder.FieldRechargeCode, field.TypeString, value)
|
||||
}
|
||||
|
||||
+18
-2
@@ -55,11 +55,13 @@ type Proxy struct {
|
||||
type ProxyEdges struct {
|
||||
// Accounts holds the value of the accounts edge.
|
||||
Accounts []*Account `json:"accounts,omitempty"`
|
||||
// PrimaryProxies holds the value of the primary_proxies edge.
|
||||
PrimaryProxies []*Proxy `json:"primary_proxies,omitempty"`
|
||||
// BackupProxy holds the value of the backup_proxy edge.
|
||||
BackupProxy *Proxy `json:"backup_proxy,omitempty"`
|
||||
// loadedTypes holds the information for reporting if a
|
||||
// type was loaded (or requested) in eager-loading or not.
|
||||
loadedTypes [2]bool
|
||||
loadedTypes [3]bool
|
||||
}
|
||||
|
||||
// AccountsOrErr returns the Accounts value or an error if the edge
|
||||
@@ -71,12 +73,21 @@ func (e ProxyEdges) AccountsOrErr() ([]*Account, error) {
|
||||
return nil, &NotLoadedError{edge: "accounts"}
|
||||
}
|
||||
|
||||
// PrimaryProxiesOrErr returns the PrimaryProxies value or an error if the edge
|
||||
// was not loaded in eager-loading.
|
||||
func (e ProxyEdges) PrimaryProxiesOrErr() ([]*Proxy, error) {
|
||||
if e.loadedTypes[1] {
|
||||
return e.PrimaryProxies, nil
|
||||
}
|
||||
return nil, &NotLoadedError{edge: "primary_proxies"}
|
||||
}
|
||||
|
||||
// BackupProxyOrErr returns the BackupProxy value or an error if the edge
|
||||
// was not loaded in eager-loading, or loaded but was not found.
|
||||
func (e ProxyEdges) BackupProxyOrErr() (*Proxy, error) {
|
||||
if e.BackupProxy != nil {
|
||||
return e.BackupProxy, nil
|
||||
} else if e.loadedTypes[1] {
|
||||
} else if e.loadedTypes[2] {
|
||||
return nil, &NotFoundError{label: proxy.Label}
|
||||
}
|
||||
return nil, &NotLoadedError{edge: "backup_proxy"}
|
||||
@@ -221,6 +232,11 @@ func (_m *Proxy) QueryAccounts() *AccountQuery {
|
||||
return NewProxyClient(_m.config).QueryAccounts(_m)
|
||||
}
|
||||
|
||||
// QueryPrimaryProxies queries the "primary_proxies" edge of the Proxy entity.
|
||||
func (_m *Proxy) QueryPrimaryProxies() *ProxyQuery {
|
||||
return NewProxyClient(_m.config).QueryPrimaryProxies(_m)
|
||||
}
|
||||
|
||||
// QueryBackupProxy queries the "backup_proxy" edge of the Proxy entity.
|
||||
func (_m *Proxy) QueryBackupProxy() *ProxyQuery {
|
||||
return NewProxyClient(_m.config).QueryBackupProxy(_m)
|
||||
|
||||
@@ -45,6 +45,8 @@ const (
|
||||
FieldExpiryWarnDays = "expiry_warn_days"
|
||||
// EdgeAccounts holds the string denoting the accounts edge name in mutations.
|
||||
EdgeAccounts = "accounts"
|
||||
// EdgePrimaryProxies holds the string denoting the primary_proxies edge name in mutations.
|
||||
EdgePrimaryProxies = "primary_proxies"
|
||||
// EdgeBackupProxy holds the string denoting the backup_proxy edge name in mutations.
|
||||
EdgeBackupProxy = "backup_proxy"
|
||||
// Table holds the table name of the proxy in the database.
|
||||
@@ -56,6 +58,10 @@ const (
|
||||
AccountsInverseTable = "accounts"
|
||||
// AccountsColumn is the table column denoting the accounts relation/edge.
|
||||
AccountsColumn = "proxy_id"
|
||||
// PrimaryProxiesTable is the table that holds the primary_proxies relation/edge.
|
||||
PrimaryProxiesTable = "proxies"
|
||||
// PrimaryProxiesColumn is the table column denoting the primary_proxies relation/edge.
|
||||
PrimaryProxiesColumn = "backup_proxy_id"
|
||||
// BackupProxyTable is the table that holds the backup_proxy relation/edge.
|
||||
BackupProxyTable = "proxies"
|
||||
// BackupProxyColumn is the table column denoting the backup_proxy relation/edge.
|
||||
@@ -219,6 +225,20 @@ func ByAccounts(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
|
||||
}
|
||||
}
|
||||
|
||||
// ByPrimaryProxiesCount orders the results by primary_proxies count.
|
||||
func ByPrimaryProxiesCount(opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
sqlgraph.OrderByNeighborsCount(s, newPrimaryProxiesStep(), opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// ByPrimaryProxies orders the results by primary_proxies terms.
|
||||
func ByPrimaryProxies(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
sqlgraph.OrderByNeighborTerms(s, newPrimaryProxiesStep(), append([]sql.OrderTerm{term}, terms...)...)
|
||||
}
|
||||
}
|
||||
|
||||
// ByBackupProxyField orders the results by backup_proxy field.
|
||||
func ByBackupProxyField(field string, opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
@@ -232,10 +252,17 @@ func newAccountsStep() *sqlgraph.Step {
|
||||
sqlgraph.Edge(sqlgraph.O2M, true, AccountsTable, AccountsColumn),
|
||||
)
|
||||
}
|
||||
func newPrimaryProxiesStep() *sqlgraph.Step {
|
||||
return sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.To(Table, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2M, true, PrimaryProxiesTable, PrimaryProxiesColumn),
|
||||
)
|
||||
}
|
||||
func newBackupProxyStep() *sqlgraph.Step {
|
||||
return sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.To(Table, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2O, false, BackupProxyTable, BackupProxyColumn),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, BackupProxyTable, BackupProxyColumn),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -913,12 +913,35 @@ func HasAccountsWith(preds ...predicate.Account) predicate.Proxy {
|
||||
})
|
||||
}
|
||||
|
||||
// HasPrimaryProxies applies the HasEdge predicate on the "primary_proxies" edge.
|
||||
func HasPrimaryProxies() predicate.Proxy {
|
||||
return predicate.Proxy(func(s *sql.Selector) {
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2M, true, PrimaryProxiesTable, PrimaryProxiesColumn),
|
||||
)
|
||||
sqlgraph.HasNeighbors(s, step)
|
||||
})
|
||||
}
|
||||
|
||||
// HasPrimaryProxiesWith applies the HasEdge predicate on the "primary_proxies" edge with a given conditions (other predicates).
|
||||
func HasPrimaryProxiesWith(preds ...predicate.Proxy) predicate.Proxy {
|
||||
return predicate.Proxy(func(s *sql.Selector) {
|
||||
step := newPrimaryProxiesStep()
|
||||
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
|
||||
for _, p := range preds {
|
||||
p(s)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// HasBackupProxy applies the HasEdge predicate on the "backup_proxy" edge.
|
||||
func HasBackupProxy() predicate.Proxy {
|
||||
return predicate.Proxy(func(s *sql.Selector) {
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2O, false, BackupProxyTable, BackupProxyColumn),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, BackupProxyTable, BackupProxyColumn),
|
||||
)
|
||||
sqlgraph.HasNeighbors(s, step)
|
||||
})
|
||||
|
||||
@@ -202,6 +202,21 @@ func (_c *ProxyCreate) AddAccounts(v ...*Account) *ProxyCreate {
|
||||
return _c.AddAccountIDs(ids...)
|
||||
}
|
||||
|
||||
// AddPrimaryProxyIDs adds the "primary_proxies" edge to the Proxy entity by IDs.
|
||||
func (_c *ProxyCreate) AddPrimaryProxyIDs(ids ...int64) *ProxyCreate {
|
||||
_c.mutation.AddPrimaryProxyIDs(ids...)
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddPrimaryProxies adds the "primary_proxies" edges to the Proxy entity.
|
||||
func (_c *ProxyCreate) AddPrimaryProxies(v ...*Proxy) *ProxyCreate {
|
||||
ids := make([]int64, len(v))
|
||||
for i := range v {
|
||||
ids[i] = v[i].ID
|
||||
}
|
||||
return _c.AddPrimaryProxyIDs(ids...)
|
||||
}
|
||||
|
||||
// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity.
|
||||
func (_c *ProxyCreate) SetBackupProxy(v *Proxy) *ProxyCreate {
|
||||
return _c.SetBackupProxyID(v.ID)
|
||||
@@ -432,13 +447,29 @@ func (_c *ProxyCreate) createSpec() (*Proxy, *sqlgraph.CreateSpec) {
|
||||
}
|
||||
_spec.Edges = append(_spec.Edges, edge)
|
||||
}
|
||||
if nodes := _c.mutation.PrimaryProxiesIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
Inverse: true,
|
||||
Table: proxy.PrimaryProxiesTable,
|
||||
Columns: []string{proxy.PrimaryProxiesColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_spec.Edges = append(_spec.Edges, edge)
|
||||
}
|
||||
if nodes := _c.mutation.BackupProxyIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2O,
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: proxy.BackupProxyTable,
|
||||
Columns: []string{proxy.BackupProxyColumn},
|
||||
Bidi: true,
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
|
||||
+92
-16
@@ -21,13 +21,14 @@ import (
|
||||
// ProxyQuery is the builder for querying Proxy entities.
|
||||
type ProxyQuery struct {
|
||||
config
|
||||
ctx *QueryContext
|
||||
order []proxy.OrderOption
|
||||
inters []Interceptor
|
||||
predicates []predicate.Proxy
|
||||
withAccounts *AccountQuery
|
||||
withBackupProxy *ProxyQuery
|
||||
modifiers []func(*sql.Selector)
|
||||
ctx *QueryContext
|
||||
order []proxy.OrderOption
|
||||
inters []Interceptor
|
||||
predicates []predicate.Proxy
|
||||
withAccounts *AccountQuery
|
||||
withPrimaryProxies *ProxyQuery
|
||||
withBackupProxy *ProxyQuery
|
||||
modifiers []func(*sql.Selector)
|
||||
// intermediate query (i.e. traversal path).
|
||||
sql *sql.Selector
|
||||
path func(context.Context) (*sql.Selector, error)
|
||||
@@ -86,6 +87,28 @@ func (_q *ProxyQuery) QueryAccounts() *AccountQuery {
|
||||
return query
|
||||
}
|
||||
|
||||
// QueryPrimaryProxies chains the current query on the "primary_proxies" edge.
|
||||
func (_q *ProxyQuery) QueryPrimaryProxies() *ProxyQuery {
|
||||
query := (&ProxyClient{config: _q.config}).Query()
|
||||
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
|
||||
if err := _q.prepareQuery(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selector := _q.sqlQuery(ctx)
|
||||
if err := selector.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(proxy.Table, proxy.FieldID, selector),
|
||||
sqlgraph.To(proxy.Table, proxy.FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2M, true, proxy.PrimaryProxiesTable, proxy.PrimaryProxiesColumn),
|
||||
)
|
||||
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
|
||||
return fromU, nil
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// QueryBackupProxy chains the current query on the "backup_proxy" edge.
|
||||
func (_q *ProxyQuery) QueryBackupProxy() *ProxyQuery {
|
||||
query := (&ProxyClient{config: _q.config}).Query()
|
||||
@@ -100,7 +123,7 @@ func (_q *ProxyQuery) QueryBackupProxy() *ProxyQuery {
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(proxy.Table, proxy.FieldID, selector),
|
||||
sqlgraph.To(proxy.Table, proxy.FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn),
|
||||
)
|
||||
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
|
||||
return fromU, nil
|
||||
@@ -295,13 +318,14 @@ func (_q *ProxyQuery) Clone() *ProxyQuery {
|
||||
return nil
|
||||
}
|
||||
return &ProxyQuery{
|
||||
config: _q.config,
|
||||
ctx: _q.ctx.Clone(),
|
||||
order: append([]proxy.OrderOption{}, _q.order...),
|
||||
inters: append([]Interceptor{}, _q.inters...),
|
||||
predicates: append([]predicate.Proxy{}, _q.predicates...),
|
||||
withAccounts: _q.withAccounts.Clone(),
|
||||
withBackupProxy: _q.withBackupProxy.Clone(),
|
||||
config: _q.config,
|
||||
ctx: _q.ctx.Clone(),
|
||||
order: append([]proxy.OrderOption{}, _q.order...),
|
||||
inters: append([]Interceptor{}, _q.inters...),
|
||||
predicates: append([]predicate.Proxy{}, _q.predicates...),
|
||||
withAccounts: _q.withAccounts.Clone(),
|
||||
withPrimaryProxies: _q.withPrimaryProxies.Clone(),
|
||||
withBackupProxy: _q.withBackupProxy.Clone(),
|
||||
// clone intermediate query.
|
||||
sql: _q.sql.Clone(),
|
||||
path: _q.path,
|
||||
@@ -319,6 +343,17 @@ func (_q *ProxyQuery) WithAccounts(opts ...func(*AccountQuery)) *ProxyQuery {
|
||||
return _q
|
||||
}
|
||||
|
||||
// WithPrimaryProxies tells the query-builder to eager-load the nodes that are connected to
|
||||
// the "primary_proxies" edge. The optional arguments are used to configure the query builder of the edge.
|
||||
func (_q *ProxyQuery) WithPrimaryProxies(opts ...func(*ProxyQuery)) *ProxyQuery {
|
||||
query := (&ProxyClient{config: _q.config}).Query()
|
||||
for _, opt := range opts {
|
||||
opt(query)
|
||||
}
|
||||
_q.withPrimaryProxies = query
|
||||
return _q
|
||||
}
|
||||
|
||||
// WithBackupProxy tells the query-builder to eager-load the nodes that are connected to
|
||||
// the "backup_proxy" edge. The optional arguments are used to configure the query builder of the edge.
|
||||
func (_q *ProxyQuery) WithBackupProxy(opts ...func(*ProxyQuery)) *ProxyQuery {
|
||||
@@ -408,8 +443,9 @@ func (_q *ProxyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proxy,
|
||||
var (
|
||||
nodes = []*Proxy{}
|
||||
_spec = _q.querySpec()
|
||||
loadedTypes = [2]bool{
|
||||
loadedTypes = [3]bool{
|
||||
_q.withAccounts != nil,
|
||||
_q.withPrimaryProxies != nil,
|
||||
_q.withBackupProxy != nil,
|
||||
}
|
||||
)
|
||||
@@ -441,6 +477,13 @@ func (_q *ProxyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proxy,
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if query := _q.withPrimaryProxies; query != nil {
|
||||
if err := _q.loadPrimaryProxies(ctx, query, nodes,
|
||||
func(n *Proxy) { n.Edges.PrimaryProxies = []*Proxy{} },
|
||||
func(n *Proxy, e *Proxy) { n.Edges.PrimaryProxies = append(n.Edges.PrimaryProxies, e) }); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if query := _q.withBackupProxy; query != nil {
|
||||
if err := _q.loadBackupProxy(ctx, query, nodes, nil,
|
||||
func(n *Proxy, e *Proxy) { n.Edges.BackupProxy = e }); err != nil {
|
||||
@@ -483,6 +526,39 @@ func (_q *ProxyQuery) loadAccounts(ctx context.Context, query *AccountQuery, nod
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (_q *ProxyQuery) loadPrimaryProxies(ctx context.Context, query *ProxyQuery, nodes []*Proxy, init func(*Proxy), assign func(*Proxy, *Proxy)) error {
|
||||
fks := make([]driver.Value, 0, len(nodes))
|
||||
nodeids := make(map[int64]*Proxy)
|
||||
for i := range nodes {
|
||||
fks = append(fks, nodes[i].ID)
|
||||
nodeids[nodes[i].ID] = nodes[i]
|
||||
if init != nil {
|
||||
init(nodes[i])
|
||||
}
|
||||
}
|
||||
if len(query.ctx.Fields) > 0 {
|
||||
query.ctx.AppendFieldOnce(proxy.FieldBackupProxyID)
|
||||
}
|
||||
query.Where(predicate.Proxy(func(s *sql.Selector) {
|
||||
s.Where(sql.InValues(s.C(proxy.PrimaryProxiesColumn), fks...))
|
||||
}))
|
||||
neighbors, err := query.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range neighbors {
|
||||
fk := n.BackupProxyID
|
||||
if fk == nil {
|
||||
return fmt.Errorf(`foreign-key "backup_proxy_id" is nil for node %v`, n.ID)
|
||||
}
|
||||
node, ok := nodeids[*fk]
|
||||
if !ok {
|
||||
return fmt.Errorf(`unexpected referenced foreign-key "backup_proxy_id" returned %v for node %v`, *fk, n.ID)
|
||||
}
|
||||
assign(node, n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (_q *ProxyQuery) loadBackupProxy(ctx context.Context, query *ProxyQuery, nodes []*Proxy, init func(*Proxy), assign func(*Proxy, *Proxy)) error {
|
||||
ids := make([]int64, 0, len(nodes))
|
||||
nodeids := make(map[int64][]*Proxy)
|
||||
|
||||
+170
-8
@@ -262,6 +262,21 @@ func (_u *ProxyUpdate) AddAccounts(v ...*Account) *ProxyUpdate {
|
||||
return _u.AddAccountIDs(ids...)
|
||||
}
|
||||
|
||||
// AddPrimaryProxyIDs adds the "primary_proxies" edge to the Proxy entity by IDs.
|
||||
func (_u *ProxyUpdate) AddPrimaryProxyIDs(ids ...int64) *ProxyUpdate {
|
||||
_u.mutation.AddPrimaryProxyIDs(ids...)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddPrimaryProxies adds the "primary_proxies" edges to the Proxy entity.
|
||||
func (_u *ProxyUpdate) AddPrimaryProxies(v ...*Proxy) *ProxyUpdate {
|
||||
ids := make([]int64, len(v))
|
||||
for i := range v {
|
||||
ids[i] = v[i].ID
|
||||
}
|
||||
return _u.AddPrimaryProxyIDs(ids...)
|
||||
}
|
||||
|
||||
// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity.
|
||||
func (_u *ProxyUpdate) SetBackupProxy(v *Proxy) *ProxyUpdate {
|
||||
return _u.SetBackupProxyID(v.ID)
|
||||
@@ -293,6 +308,27 @@ func (_u *ProxyUpdate) RemoveAccounts(v ...*Account) *ProxyUpdate {
|
||||
return _u.RemoveAccountIDs(ids...)
|
||||
}
|
||||
|
||||
// ClearPrimaryProxies clears all "primary_proxies" edges to the Proxy entity.
|
||||
func (_u *ProxyUpdate) ClearPrimaryProxies() *ProxyUpdate {
|
||||
_u.mutation.ClearPrimaryProxies()
|
||||
return _u
|
||||
}
|
||||
|
||||
// RemovePrimaryProxyIDs removes the "primary_proxies" edge to Proxy entities by IDs.
|
||||
func (_u *ProxyUpdate) RemovePrimaryProxyIDs(ids ...int64) *ProxyUpdate {
|
||||
_u.mutation.RemovePrimaryProxyIDs(ids...)
|
||||
return _u
|
||||
}
|
||||
|
||||
// RemovePrimaryProxies removes "primary_proxies" edges to Proxy entities.
|
||||
func (_u *ProxyUpdate) RemovePrimaryProxies(v ...*Proxy) *ProxyUpdate {
|
||||
ids := make([]int64, len(v))
|
||||
for i := range v {
|
||||
ids[i] = v[i].ID
|
||||
}
|
||||
return _u.RemovePrimaryProxyIDs(ids...)
|
||||
}
|
||||
|
||||
// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity.
|
||||
func (_u *ProxyUpdate) ClearBackupProxy() *ProxyUpdate {
|
||||
_u.mutation.ClearBackupProxy()
|
||||
@@ -492,13 +528,58 @@ func (_u *ProxyUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
}
|
||||
_spec.Edges.Add = append(_spec.Edges.Add, edge)
|
||||
}
|
||||
if _u.mutation.PrimaryProxiesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
Inverse: true,
|
||||
Table: proxy.PrimaryProxiesTable,
|
||||
Columns: []string{proxy.PrimaryProxiesColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
|
||||
}
|
||||
if nodes := _u.mutation.RemovedPrimaryProxiesIDs(); len(nodes) > 0 && !_u.mutation.PrimaryProxiesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
Inverse: true,
|
||||
Table: proxy.PrimaryProxiesTable,
|
||||
Columns: []string{proxy.PrimaryProxiesColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
|
||||
}
|
||||
if nodes := _u.mutation.PrimaryProxiesIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
Inverse: true,
|
||||
Table: proxy.PrimaryProxiesTable,
|
||||
Columns: []string{proxy.PrimaryProxiesColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_spec.Edges.Add = append(_spec.Edges.Add, edge)
|
||||
}
|
||||
if _u.mutation.BackupProxyCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2O,
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: proxy.BackupProxyTable,
|
||||
Columns: []string{proxy.BackupProxyColumn},
|
||||
Bidi: true,
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
@@ -507,11 +588,11 @@ func (_u *ProxyUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
}
|
||||
if nodes := _u.mutation.BackupProxyIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2O,
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: proxy.BackupProxyTable,
|
||||
Columns: []string{proxy.BackupProxyColumn},
|
||||
Bidi: true,
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
@@ -774,6 +855,21 @@ func (_u *ProxyUpdateOne) AddAccounts(v ...*Account) *ProxyUpdateOne {
|
||||
return _u.AddAccountIDs(ids...)
|
||||
}
|
||||
|
||||
// AddPrimaryProxyIDs adds the "primary_proxies" edge to the Proxy entity by IDs.
|
||||
func (_u *ProxyUpdateOne) AddPrimaryProxyIDs(ids ...int64) *ProxyUpdateOne {
|
||||
_u.mutation.AddPrimaryProxyIDs(ids...)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddPrimaryProxies adds the "primary_proxies" edges to the Proxy entity.
|
||||
func (_u *ProxyUpdateOne) AddPrimaryProxies(v ...*Proxy) *ProxyUpdateOne {
|
||||
ids := make([]int64, len(v))
|
||||
for i := range v {
|
||||
ids[i] = v[i].ID
|
||||
}
|
||||
return _u.AddPrimaryProxyIDs(ids...)
|
||||
}
|
||||
|
||||
// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity.
|
||||
func (_u *ProxyUpdateOne) SetBackupProxy(v *Proxy) *ProxyUpdateOne {
|
||||
return _u.SetBackupProxyID(v.ID)
|
||||
@@ -805,6 +901,27 @@ func (_u *ProxyUpdateOne) RemoveAccounts(v ...*Account) *ProxyUpdateOne {
|
||||
return _u.RemoveAccountIDs(ids...)
|
||||
}
|
||||
|
||||
// ClearPrimaryProxies clears all "primary_proxies" edges to the Proxy entity.
|
||||
func (_u *ProxyUpdateOne) ClearPrimaryProxies() *ProxyUpdateOne {
|
||||
_u.mutation.ClearPrimaryProxies()
|
||||
return _u
|
||||
}
|
||||
|
||||
// RemovePrimaryProxyIDs removes the "primary_proxies" edge to Proxy entities by IDs.
|
||||
func (_u *ProxyUpdateOne) RemovePrimaryProxyIDs(ids ...int64) *ProxyUpdateOne {
|
||||
_u.mutation.RemovePrimaryProxyIDs(ids...)
|
||||
return _u
|
||||
}
|
||||
|
||||
// RemovePrimaryProxies removes "primary_proxies" edges to Proxy entities.
|
||||
func (_u *ProxyUpdateOne) RemovePrimaryProxies(v ...*Proxy) *ProxyUpdateOne {
|
||||
ids := make([]int64, len(v))
|
||||
for i := range v {
|
||||
ids[i] = v[i].ID
|
||||
}
|
||||
return _u.RemovePrimaryProxyIDs(ids...)
|
||||
}
|
||||
|
||||
// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity.
|
||||
func (_u *ProxyUpdateOne) ClearBackupProxy() *ProxyUpdateOne {
|
||||
_u.mutation.ClearBackupProxy()
|
||||
@@ -1034,13 +1151,58 @@ func (_u *ProxyUpdateOne) sqlSave(ctx context.Context) (_node *Proxy, err error)
|
||||
}
|
||||
_spec.Edges.Add = append(_spec.Edges.Add, edge)
|
||||
}
|
||||
if _u.mutation.PrimaryProxiesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
Inverse: true,
|
||||
Table: proxy.PrimaryProxiesTable,
|
||||
Columns: []string{proxy.PrimaryProxiesColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
|
||||
}
|
||||
if nodes := _u.mutation.RemovedPrimaryProxiesIDs(); len(nodes) > 0 && !_u.mutation.PrimaryProxiesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
Inverse: true,
|
||||
Table: proxy.PrimaryProxiesTable,
|
||||
Columns: []string{proxy.PrimaryProxiesColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
|
||||
}
|
||||
if nodes := _u.mutation.PrimaryProxiesIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
Inverse: true,
|
||||
Table: proxy.PrimaryProxiesTable,
|
||||
Columns: []string{proxy.PrimaryProxiesColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_spec.Edges.Add = append(_spec.Edges.Add, edge)
|
||||
}
|
||||
if _u.mutation.BackupProxyCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2O,
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: proxy.BackupProxyTable,
|
||||
Columns: []string{proxy.BackupProxyColumn},
|
||||
Bidi: true,
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
@@ -1049,11 +1211,11 @@ func (_u *ProxyUpdateOne) sqlSave(ctx context.Context) (_node *Proxy, err error)
|
||||
}
|
||||
if nodes := _u.mutation.BackupProxyIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2O,
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: proxy.BackupProxyTable,
|
||||
Columns: []string{proxy.BackupProxyColumn},
|
||||
Bidi: true,
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64),
|
||||
},
|
||||
|
||||
@@ -1157,52 +1157,70 @@ func init() {
|
||||
groupDescAllowLive := groupFields[47].Descriptor()
|
||||
// group.DefaultAllowLive holds the default value on creation for the allow_live field.
|
||||
group.DefaultAllowLive = groupDescAllowLive.Default.(bool)
|
||||
// groupDescForceOpenaiFast is the schema descriptor for force_openai_fast field.
|
||||
groupDescForceOpenaiFast := groupFields[48].Descriptor()
|
||||
// group.DefaultForceOpenaiFast holds the default value on creation for the force_openai_fast field.
|
||||
group.DefaultForceOpenaiFast = groupDescForceOpenaiFast.Default.(bool)
|
||||
// groupDescFreeOpenaiFast is the schema descriptor for free_openai_fast field.
|
||||
groupDescFreeOpenaiFast := groupFields[49].Descriptor()
|
||||
// group.DefaultFreeOpenaiFast holds the default value on creation for the free_openai_fast field.
|
||||
group.DefaultFreeOpenaiFast = groupDescFreeOpenaiFast.Default.(bool)
|
||||
// groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field.
|
||||
groupDescRequireOauthOnly := groupFields[48].Descriptor()
|
||||
groupDescRequireOauthOnly := groupFields[50].Descriptor()
|
||||
// group.DefaultRequireOauthOnly holds the default value on creation for the require_oauth_only field.
|
||||
group.DefaultRequireOauthOnly = groupDescRequireOauthOnly.Default.(bool)
|
||||
// groupDescRequirePrivacySet is the schema descriptor for require_privacy_set field.
|
||||
groupDescRequirePrivacySet := groupFields[49].Descriptor()
|
||||
groupDescRequirePrivacySet := groupFields[51].Descriptor()
|
||||
// group.DefaultRequirePrivacySet holds the default value on creation for the require_privacy_set field.
|
||||
group.DefaultRequirePrivacySet = groupDescRequirePrivacySet.Default.(bool)
|
||||
// groupDescDefaultMappedModel is the schema descriptor for default_mapped_model field.
|
||||
groupDescDefaultMappedModel := groupFields[50].Descriptor()
|
||||
groupDescDefaultMappedModel := groupFields[52].Descriptor()
|
||||
// group.DefaultDefaultMappedModel holds the default value on creation for the default_mapped_model field.
|
||||
group.DefaultDefaultMappedModel = groupDescDefaultMappedModel.Default.(string)
|
||||
// group.DefaultMappedModelValidator is a validator for the "default_mapped_model" field. It is called by the builders before save.
|
||||
group.DefaultMappedModelValidator = groupDescDefaultMappedModel.Validators[0].(func(string) error)
|
||||
// groupDescMessagesDispatchModelConfig is the schema descriptor for messages_dispatch_model_config field.
|
||||
groupDescMessagesDispatchModelConfig := groupFields[51].Descriptor()
|
||||
groupDescMessagesDispatchModelConfig := groupFields[53].Descriptor()
|
||||
// group.DefaultMessagesDispatchModelConfig holds the default value on creation for the messages_dispatch_model_config field.
|
||||
group.DefaultMessagesDispatchModelConfig = groupDescMessagesDispatchModelConfig.Default.(domain.OpenAIMessagesDispatchModelConfig)
|
||||
// groupDescModelsListConfig is the schema descriptor for models_list_config field.
|
||||
groupDescModelsListConfig := groupFields[52].Descriptor()
|
||||
// group.DefaultModelsListConfig holds the default value on creation for the models_list_config field.
|
||||
group.DefaultModelsListConfig = groupDescModelsListConfig.Default.(domain.GroupModelsListConfig)
|
||||
// groupDescModelAllowlist is the schema descriptor for model_allowlist field.
|
||||
groupDescModelAllowlist := groupFields[54].Descriptor()
|
||||
// group.DefaultModelAllowlist holds the default value on creation for the model_allowlist field.
|
||||
group.DefaultModelAllowlist = groupDescModelAllowlist.Default.(domain.GroupModelAllowlist)
|
||||
// groupDescCodexModelsManifestConfig is the schema descriptor for codex_models_manifest_config field.
|
||||
groupDescCodexModelsManifestConfig := groupFields[55].Descriptor()
|
||||
// group.DefaultCodexModelsManifestConfig holds the default value on creation for the codex_models_manifest_config field.
|
||||
group.DefaultCodexModelsManifestConfig = groupDescCodexModelsManifestConfig.Default.(domain.GroupCodexModelsManifestConfig)
|
||||
// groupDescRpmLimit is the schema descriptor for rpm_limit field.
|
||||
groupDescRpmLimit := groupFields[53].Descriptor()
|
||||
groupDescRpmLimit := groupFields[56].Descriptor()
|
||||
// group.DefaultRpmLimit holds the default value on creation for the rpm_limit field.
|
||||
group.DefaultRpmLimit = groupDescRpmLimit.Default.(int)
|
||||
// groupDescMaxReasoningEffort is the schema descriptor for max_reasoning_effort field.
|
||||
groupDescMaxReasoningEffort := groupFields[54].Descriptor()
|
||||
groupDescMaxReasoningEffort := groupFields[57].Descriptor()
|
||||
// group.DefaultMaxReasoningEffort holds the default value on creation for the max_reasoning_effort field.
|
||||
group.DefaultMaxReasoningEffort = groupDescMaxReasoningEffort.Default.(string)
|
||||
// group.MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
|
||||
group.MaxReasoningEffortValidator = groupDescMaxReasoningEffort.Validators[0].(func(string) error)
|
||||
// groupDescMaxReasoningEffortOverLimit is the schema descriptor for max_reasoning_effort_over_limit field.
|
||||
groupDescMaxReasoningEffortOverLimit := groupFields[58].Descriptor()
|
||||
// group.DefaultMaxReasoningEffortOverLimit holds the default value on creation for the max_reasoning_effort_over_limit field.
|
||||
group.DefaultMaxReasoningEffortOverLimit = groupDescMaxReasoningEffortOverLimit.Default.(string)
|
||||
// group.MaxReasoningEffortOverLimitValidator is a validator for the "max_reasoning_effort_over_limit" field. It is called by the builders before save.
|
||||
group.MaxReasoningEffortOverLimitValidator = groupDescMaxReasoningEffortOverLimit.Validators[0].(func(string) error)
|
||||
// groupDescReasoningEffortMappings is the schema descriptor for reasoning_effort_mappings field.
|
||||
groupDescReasoningEffortMappings := groupFields[55].Descriptor()
|
||||
groupDescReasoningEffortMappings := groupFields[59].Descriptor()
|
||||
// group.DefaultReasoningEffortMappings holds the default value on creation for the reasoning_effort_mappings field.
|
||||
group.DefaultReasoningEffortMappings = groupDescReasoningEffortMappings.Default.([]domain.ReasoningEffortMapping)
|
||||
// groupDescProfitControlEnabled is the schema descriptor for profit_control_enabled field.
|
||||
groupDescProfitControlEnabled := groupFields[56].Descriptor()
|
||||
groupDescProfitControlEnabled := groupFields[60].Descriptor()
|
||||
// group.DefaultProfitControlEnabled holds the default value on creation for the profit_control_enabled field.
|
||||
group.DefaultProfitControlEnabled = groupDescProfitControlEnabled.Default.(bool)
|
||||
// groupDescProfitMinMargin is the schema descriptor for profit_min_margin field.
|
||||
groupDescProfitMinMargin := groupFields[57].Descriptor()
|
||||
groupDescProfitMinMargin := groupFields[61].Descriptor()
|
||||
// group.DefaultProfitMinMargin holds the default value on creation for the profit_min_margin field.
|
||||
group.DefaultProfitMinMargin = groupDescProfitMinMargin.Default.(float64)
|
||||
// groupDescProfitSafetyBuffer is the schema descriptor for profit_safety_buffer field.
|
||||
groupDescProfitSafetyBuffer := groupFields[58].Descriptor()
|
||||
groupDescProfitSafetyBuffer := groupFields[62].Descriptor()
|
||||
// group.DefaultProfitSafetyBuffer holds the default value on creation for the profit_safety_buffer field.
|
||||
group.DefaultProfitSafetyBuffer = groupDescProfitSafetyBuffer.Default.(float64)
|
||||
idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin()
|
||||
@@ -1305,70 +1323,74 @@ func init() {
|
||||
paymentorderDescFeeRate := paymentorderFields[6].Descriptor()
|
||||
// paymentorder.DefaultFeeRate holds the default value on creation for the fee_rate field.
|
||||
paymentorder.DefaultFeeRate = paymentorderDescFeeRate.Default.(float64)
|
||||
// paymentorderDescBonusAmount is the schema descriptor for bonus_amount field.
|
||||
paymentorderDescBonusAmount := paymentorderFields[7].Descriptor()
|
||||
// paymentorder.DefaultBonusAmount holds the default value on creation for the bonus_amount field.
|
||||
paymentorder.DefaultBonusAmount = paymentorderDescBonusAmount.Default.(float64)
|
||||
// paymentorderDescRechargeCode is the schema descriptor for recharge_code field.
|
||||
paymentorderDescRechargeCode := paymentorderFields[7].Descriptor()
|
||||
paymentorderDescRechargeCode := paymentorderFields[8].Descriptor()
|
||||
// paymentorder.RechargeCodeValidator is a validator for the "recharge_code" field. It is called by the builders before save.
|
||||
paymentorder.RechargeCodeValidator = paymentorderDescRechargeCode.Validators[0].(func(string) error)
|
||||
// paymentorderDescOutTradeNo is the schema descriptor for out_trade_no field.
|
||||
paymentorderDescOutTradeNo := paymentorderFields[8].Descriptor()
|
||||
paymentorderDescOutTradeNo := paymentorderFields[9].Descriptor()
|
||||
// paymentorder.DefaultOutTradeNo holds the default value on creation for the out_trade_no field.
|
||||
paymentorder.DefaultOutTradeNo = paymentorderDescOutTradeNo.Default.(string)
|
||||
// paymentorder.OutTradeNoValidator is a validator for the "out_trade_no" field. It is called by the builders before save.
|
||||
paymentorder.OutTradeNoValidator = paymentorderDescOutTradeNo.Validators[0].(func(string) error)
|
||||
// paymentorderDescPaymentType is the schema descriptor for payment_type field.
|
||||
paymentorderDescPaymentType := paymentorderFields[9].Descriptor()
|
||||
paymentorderDescPaymentType := paymentorderFields[10].Descriptor()
|
||||
// paymentorder.PaymentTypeValidator is a validator for the "payment_type" field. It is called by the builders before save.
|
||||
paymentorder.PaymentTypeValidator = paymentorderDescPaymentType.Validators[0].(func(string) error)
|
||||
// paymentorderDescPaymentTradeNo is the schema descriptor for payment_trade_no field.
|
||||
paymentorderDescPaymentTradeNo := paymentorderFields[10].Descriptor()
|
||||
paymentorderDescPaymentTradeNo := paymentorderFields[11].Descriptor()
|
||||
// paymentorder.PaymentTradeNoValidator is a validator for the "payment_trade_no" field. It is called by the builders before save.
|
||||
paymentorder.PaymentTradeNoValidator = paymentorderDescPaymentTradeNo.Validators[0].(func(string) error)
|
||||
// paymentorderDescOrderType is the schema descriptor for order_type field.
|
||||
paymentorderDescOrderType := paymentorderFields[14].Descriptor()
|
||||
paymentorderDescOrderType := paymentorderFields[15].Descriptor()
|
||||
// paymentorder.DefaultOrderType holds the default value on creation for the order_type field.
|
||||
paymentorder.DefaultOrderType = paymentorderDescOrderType.Default.(string)
|
||||
// paymentorder.OrderTypeValidator is a validator for the "order_type" field. It is called by the builders before save.
|
||||
paymentorder.OrderTypeValidator = paymentorderDescOrderType.Validators[0].(func(string) error)
|
||||
// paymentorderDescProviderInstanceID is the schema descriptor for provider_instance_id field.
|
||||
paymentorderDescProviderInstanceID := paymentorderFields[18].Descriptor()
|
||||
paymentorderDescProviderInstanceID := paymentorderFields[19].Descriptor()
|
||||
// paymentorder.ProviderInstanceIDValidator is a validator for the "provider_instance_id" field. It is called by the builders before save.
|
||||
paymentorder.ProviderInstanceIDValidator = paymentorderDescProviderInstanceID.Validators[0].(func(string) error)
|
||||
// paymentorderDescProviderKey is the schema descriptor for provider_key field.
|
||||
paymentorderDescProviderKey := paymentorderFields[19].Descriptor()
|
||||
paymentorderDescProviderKey := paymentorderFields[20].Descriptor()
|
||||
// paymentorder.ProviderKeyValidator is a validator for the "provider_key" field. It is called by the builders before save.
|
||||
paymentorder.ProviderKeyValidator = paymentorderDescProviderKey.Validators[0].(func(string) error)
|
||||
// paymentorderDescStatus is the schema descriptor for status field.
|
||||
paymentorderDescStatus := paymentorderFields[21].Descriptor()
|
||||
paymentorderDescStatus := paymentorderFields[22].Descriptor()
|
||||
// paymentorder.DefaultStatus holds the default value on creation for the status field.
|
||||
paymentorder.DefaultStatus = paymentorderDescStatus.Default.(string)
|
||||
// paymentorder.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
||||
paymentorder.StatusValidator = paymentorderDescStatus.Validators[0].(func(string) error)
|
||||
// paymentorderDescRefundAmount is the schema descriptor for refund_amount field.
|
||||
paymentorderDescRefundAmount := paymentorderFields[22].Descriptor()
|
||||
paymentorderDescRefundAmount := paymentorderFields[23].Descriptor()
|
||||
// paymentorder.DefaultRefundAmount holds the default value on creation for the refund_amount field.
|
||||
paymentorder.DefaultRefundAmount = paymentorderDescRefundAmount.Default.(float64)
|
||||
// paymentorderDescForceRefund is the schema descriptor for force_refund field.
|
||||
paymentorderDescForceRefund := paymentorderFields[25].Descriptor()
|
||||
paymentorderDescForceRefund := paymentorderFields[26].Descriptor()
|
||||
// paymentorder.DefaultForceRefund holds the default value on creation for the force_refund field.
|
||||
paymentorder.DefaultForceRefund = paymentorderDescForceRefund.Default.(bool)
|
||||
// paymentorderDescRefundRequestedBy is the schema descriptor for refund_requested_by field.
|
||||
paymentorderDescRefundRequestedBy := paymentorderFields[28].Descriptor()
|
||||
paymentorderDescRefundRequestedBy := paymentorderFields[29].Descriptor()
|
||||
// paymentorder.RefundRequestedByValidator is a validator for the "refund_requested_by" field. It is called by the builders before save.
|
||||
paymentorder.RefundRequestedByValidator = paymentorderDescRefundRequestedBy.Validators[0].(func(string) error)
|
||||
// paymentorderDescClientIP is the schema descriptor for client_ip field.
|
||||
paymentorderDescClientIP := paymentorderFields[34].Descriptor()
|
||||
paymentorderDescClientIP := paymentorderFields[35].Descriptor()
|
||||
// paymentorder.ClientIPValidator is a validator for the "client_ip" field. It is called by the builders before save.
|
||||
paymentorder.ClientIPValidator = paymentorderDescClientIP.Validators[0].(func(string) error)
|
||||
// paymentorderDescSrcHost is the schema descriptor for src_host field.
|
||||
paymentorderDescSrcHost := paymentorderFields[35].Descriptor()
|
||||
paymentorderDescSrcHost := paymentorderFields[36].Descriptor()
|
||||
// paymentorder.SrcHostValidator is a validator for the "src_host" field. It is called by the builders before save.
|
||||
paymentorder.SrcHostValidator = paymentorderDescSrcHost.Validators[0].(func(string) error)
|
||||
// paymentorderDescCreatedAt is the schema descriptor for created_at field.
|
||||
paymentorderDescCreatedAt := paymentorderFields[37].Descriptor()
|
||||
paymentorderDescCreatedAt := paymentorderFields[38].Descriptor()
|
||||
// paymentorder.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
paymentorder.DefaultCreatedAt = paymentorderDescCreatedAt.Default.(func() time.Time)
|
||||
// paymentorderDescUpdatedAt is the schema descriptor for updated_at field.
|
||||
paymentorderDescUpdatedAt := paymentorderFields[38].Descriptor()
|
||||
paymentorderDescUpdatedAt := paymentorderFields[39].Descriptor()
|
||||
// paymentorder.DefaultUpdatedAt holds the default value on creation for the updated_at field.
|
||||
paymentorder.DefaultUpdatedAt = paymentorderDescUpdatedAt.Default.(func() time.Time)
|
||||
// paymentorder.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
||||
@@ -2217,24 +2239,28 @@ func init() {
|
||||
user.DefaultSignupSource = userDescSignupSource.Default.(string)
|
||||
// user.SignupSourceValidator is a validator for the "signup_source" field. It is called by the builders before save.
|
||||
user.SignupSourceValidator = userDescSignupSource.Validators[0].(func(string) error)
|
||||
// userDescRestrictPublicGroups is the schema descriptor for restrict_public_groups field.
|
||||
userDescRestrictPublicGroups := userFields[15].Descriptor()
|
||||
// user.DefaultRestrictPublicGroups holds the default value on creation for the restrict_public_groups field.
|
||||
user.DefaultRestrictPublicGroups = userDescRestrictPublicGroups.Default.(bool)
|
||||
// userDescBalanceNotifyEnabled is the schema descriptor for balance_notify_enabled field.
|
||||
userDescBalanceNotifyEnabled := userFields[15].Descriptor()
|
||||
userDescBalanceNotifyEnabled := userFields[16].Descriptor()
|
||||
// user.DefaultBalanceNotifyEnabled holds the default value on creation for the balance_notify_enabled field.
|
||||
user.DefaultBalanceNotifyEnabled = userDescBalanceNotifyEnabled.Default.(bool)
|
||||
// userDescBalanceNotifyThresholdType is the schema descriptor for balance_notify_threshold_type field.
|
||||
userDescBalanceNotifyThresholdType := userFields[16].Descriptor()
|
||||
userDescBalanceNotifyThresholdType := userFields[17].Descriptor()
|
||||
// user.DefaultBalanceNotifyThresholdType holds the default value on creation for the balance_notify_threshold_type field.
|
||||
user.DefaultBalanceNotifyThresholdType = userDescBalanceNotifyThresholdType.Default.(string)
|
||||
// userDescBalanceNotifyExtraEmails is the schema descriptor for balance_notify_extra_emails field.
|
||||
userDescBalanceNotifyExtraEmails := userFields[18].Descriptor()
|
||||
userDescBalanceNotifyExtraEmails := userFields[19].Descriptor()
|
||||
// user.DefaultBalanceNotifyExtraEmails holds the default value on creation for the balance_notify_extra_emails field.
|
||||
user.DefaultBalanceNotifyExtraEmails = userDescBalanceNotifyExtraEmails.Default.(string)
|
||||
// userDescTotalRecharged is the schema descriptor for total_recharged field.
|
||||
userDescTotalRecharged := userFields[19].Descriptor()
|
||||
userDescTotalRecharged := userFields[20].Descriptor()
|
||||
// user.DefaultTotalRecharged holds the default value on creation for the total_recharged field.
|
||||
user.DefaultTotalRecharged = userDescTotalRecharged.Default.(float64)
|
||||
// userDescRpmLimit is the schema descriptor for rpm_limit field.
|
||||
userDescRpmLimit := userFields[20].Descriptor()
|
||||
userDescRpmLimit := userFields[21].Descriptor()
|
||||
// user.DefaultRpmLimit holds the default value on creation for the rpm_limit field.
|
||||
user.DefaultRpmLimit = userDescRpmLimit.Default.(int)
|
||||
userallowedgroupFields := schema.UserAllowedGroup{}.Fields()
|
||||
|
||||
@@ -36,7 +36,7 @@ func (ChannelMonitor) Fields() []ent.Field {
|
||||
MaxLen(100),
|
||||
field.Enum("provider").
|
||||
Values("openai", "anthropic", "gemini", "grok",
|
||||
"antigravity", "kimi", "zhipu", "deepseek"),
|
||||
"antigravity", "kimi", "zhipu", "deepseek", "minimax", "opencode_go"),
|
||||
// check_mode: 'probe' | 'quota' | 'quota_probe'
|
||||
// probe - LLM 探活(默认,原有行为)
|
||||
// quota - 仅查关联账号的用量/余额(零 LLM 成本;endpoint/api_key 可空)
|
||||
|
||||
@@ -40,7 +40,7 @@ func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
|
||||
MaxLen(100),
|
||||
field.Enum("provider").
|
||||
Values("openai", "anthropic", "gemini", "grok",
|
||||
"antigravity", "kimi", "zhipu", "deepseek"),
|
||||
"antigravity", "kimi", "zhipu", "deepseek", "minimax", "opencode_go"),
|
||||
field.String("api_mode").
|
||||
Default("chat_completions").
|
||||
MaxLen(32).
|
||||
|
||||
@@ -242,6 +242,12 @@ func (Group) Fields() []ent.Field {
|
||||
field.Bool("allow_live").
|
||||
Default(false).
|
||||
Comment("是否允许此 OpenAI 分组访问 Live 接口"),
|
||||
field.Bool("force_openai_fast").
|
||||
Default(false).
|
||||
Comment("是否强制此 OpenAI/Composite 分组请求使用 service_tier=priority"),
|
||||
field.Bool("free_openai_fast").
|
||||
Default(false).
|
||||
Comment("是否让此 OpenAI/Composite 分组的 Fast 请求按 Standard 价格计费"),
|
||||
field.Bool("require_oauth_only").
|
||||
Default(false).
|
||||
Comment("仅允许非 apikey 类型账号关联到此分组"),
|
||||
@@ -256,10 +262,14 @@ func (Group) Fields() []ent.Field {
|
||||
Default(domain.OpenAIMessagesDispatchModelConfig{}).
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("OpenAI Messages 调度模型配置:按 Claude 系列/精确模型映射到目标 GPT 模型"),
|
||||
field.JSON("models_list_config", domain.GroupModelsListConfig{}).
|
||||
Default(domain.GroupModelsListConfig{}).
|
||||
field.JSON("model_allowlist", domain.GroupModelAllowlist{}).
|
||||
Default(domain.GroupModelAllowlist{}).
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("自定义 /v1/models 展示列表配置;仅影响模型列表响应,不影响调度"),
|
||||
Comment("分组模型白名单:同时约束模型列表接口与请求准入"),
|
||||
field.JSON("codex_models_manifest_config", domain.GroupCodexModelsManifestConfig{}).
|
||||
Default(domain.GroupCodexModelsManifestConfig{}).
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("固定账号获取 Codex Model Manifest 配置;开启后 /models 请求只用选定账号拉取(仅 openai 平台)"),
|
||||
|
||||
// 分组级每分钟请求数上限(0 = 不限制)。设置后优先于用户级兜底生效。
|
||||
field.Int("rpm_limit").
|
||||
@@ -271,10 +281,14 @@ func (Group) Fields() []ent.Field {
|
||||
MaxLen(20).
|
||||
Default("").
|
||||
Comment("OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max"),
|
||||
field.String("max_reasoning_effort_over_limit").
|
||||
MaxLen(20).
|
||||
Default("downgrade").
|
||||
Comment("超过推理强度上限时的访问控制:downgrade 自动降档,deny 拒绝访问"),
|
||||
field.JSON("reasoning_effort_mappings", []domain.ReasoningEffortMapping{}).
|
||||
Default([]domain.ReasoningEffortMapping{}).
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("OpenAI reasoning effort 自定义精确映射;先映射再应用上限"),
|
||||
Comment("OpenAI reasoning effort 自定义映射;可按模型精确名、前缀或后缀限定,先映射再应用上限"),
|
||||
|
||||
// 分组利润控制(migration 192/193):openai/anthropic/gemini/grok/antigravity
|
||||
// 的 token 分组可启用,composite 分组不能直接启用。
|
||||
|
||||
@@ -50,6 +50,10 @@ func (PaymentOrder) Fields() []ent.Field {
|
||||
field.Float("fee_rate").
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}).
|
||||
Default(0),
|
||||
// 充值赠送额度(USD)。已计入 amount;单独记录用于订单展示与推广返利基数剔除。
|
||||
field.Float("bonus_amount").
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,2)"}).
|
||||
Default(0),
|
||||
field.String("recharge_code").
|
||||
MaxLen(64),
|
||||
|
||||
|
||||
@@ -73,6 +73,10 @@ func (Proxy) Edges() []ent.Edge {
|
||||
// accounts: 使用此代理的账户(反向边)
|
||||
edge.From("accounts", Account.Type).
|
||||
Ref("proxy"),
|
||||
// Directed many-to-one: a backup can serve multiple primary proxies.
|
||||
// The inverse edge prevents Ent from treating this self-reference as symmetric.
|
||||
edge.From("primary_proxies", Proxy.Type).
|
||||
Ref("backup_proxy"),
|
||||
edge.To("backup_proxy", Proxy.Type).
|
||||
Field("backup_proxy_id").
|
||||
Unique(),
|
||||
|
||||
@@ -96,6 +96,11 @@ func (User) Fields() []ent.Field {
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||
|
||||
// 公开分组访问限制:为 false 时用户可绑定任意非专属分组(默认行为),
|
||||
// 为 true 时仅可绑定 user_allowed_groups 中列出的公开分组。
|
||||
field.Bool("restrict_public_groups").
|
||||
Default(false),
|
||||
|
||||
// 余额不足通知
|
||||
field.Bool("balance_notify_enabled").
|
||||
Default(true),
|
||||
|
||||
@@ -42,7 +42,7 @@ func (UserPlatformQuota) Fields() []ent.Field {
|
||||
// 此处为 ent 构建期约束,需与 service.AllowedQuotaPlatforms 保持同步。
|
||||
switch s {
|
||||
case "anthropic", "openai", "gemini", "antigravity", "grok",
|
||||
"kimi", "zhipu", "deepseek":
|
||||
"kimi", "zhipu", "deepseek", "minimax", "opencode_go", "typesafe":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("platform %q is not allowed", s)
|
||||
|
||||
+12
-1
@@ -53,6 +53,8 @@ type User struct {
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
// LastActiveAt holds the value of the "last_active_at" field.
|
||||
LastActiveAt *time.Time `json:"last_active_at,omitempty"`
|
||||
// RestrictPublicGroups holds the value of the "restrict_public_groups" field.
|
||||
RestrictPublicGroups bool `json:"restrict_public_groups,omitempty"`
|
||||
// BalanceNotifyEnabled holds the value of the "balance_notify_enabled" field.
|
||||
BalanceNotifyEnabled bool `json:"balance_notify_enabled,omitempty"`
|
||||
// BalanceNotifyThresholdType holds the value of the "balance_notify_threshold_type" field.
|
||||
@@ -237,7 +239,7 @@ func (*User) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case user.FieldTotpEnabled, user.FieldBalanceNotifyEnabled:
|
||||
case user.FieldTotpEnabled, user.FieldRestrictPublicGroups, user.FieldBalanceNotifyEnabled:
|
||||
values[i] = new(sql.NullBool)
|
||||
case user.FieldBalance, user.FieldFrozenBalance, user.FieldBalanceNotifyThreshold, user.FieldTotalRecharged:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
@@ -381,6 +383,12 @@ func (_m *User) assignValues(columns []string, values []any) error {
|
||||
_m.LastActiveAt = new(time.Time)
|
||||
*_m.LastActiveAt = value.Time
|
||||
}
|
||||
case user.FieldRestrictPublicGroups:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field restrict_public_groups", values[i])
|
||||
} else if value.Valid {
|
||||
_m.RestrictPublicGroups = value.Bool
|
||||
}
|
||||
case user.FieldBalanceNotifyEnabled:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field balance_notify_enabled", values[i])
|
||||
@@ -588,6 +596,9 @@ func (_m *User) String() string {
|
||||
builder.WriteString(v.Format(time.ANSIC))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("restrict_public_groups=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.RestrictPublicGroups))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("balance_notify_enabled=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.BalanceNotifyEnabled))
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -51,6 +51,8 @@ const (
|
||||
FieldLastLoginAt = "last_login_at"
|
||||
// FieldLastActiveAt holds the string denoting the last_active_at field in the database.
|
||||
FieldLastActiveAt = "last_active_at"
|
||||
// FieldRestrictPublicGroups holds the string denoting the restrict_public_groups field in the database.
|
||||
FieldRestrictPublicGroups = "restrict_public_groups"
|
||||
// FieldBalanceNotifyEnabled holds the string denoting the balance_notify_enabled field in the database.
|
||||
FieldBalanceNotifyEnabled = "balance_notify_enabled"
|
||||
// FieldBalanceNotifyThresholdType holds the string denoting the balance_notify_threshold_type field in the database.
|
||||
@@ -212,6 +214,7 @@ var Columns = []string{
|
||||
FieldSignupSource,
|
||||
FieldLastLoginAt,
|
||||
FieldLastActiveAt,
|
||||
FieldRestrictPublicGroups,
|
||||
FieldBalanceNotifyEnabled,
|
||||
FieldBalanceNotifyThresholdType,
|
||||
FieldBalanceNotifyThreshold,
|
||||
@@ -280,6 +283,8 @@ var (
|
||||
DefaultSignupSource string
|
||||
// SignupSourceValidator is a validator for the "signup_source" field. It is called by the builders before save.
|
||||
SignupSourceValidator func(string) error
|
||||
// DefaultRestrictPublicGroups holds the default value on creation for the "restrict_public_groups" field.
|
||||
DefaultRestrictPublicGroups bool
|
||||
// DefaultBalanceNotifyEnabled holds the default value on creation for the "balance_notify_enabled" field.
|
||||
DefaultBalanceNotifyEnabled bool
|
||||
// DefaultBalanceNotifyThresholdType holds the default value on creation for the "balance_notify_threshold_type" field.
|
||||
@@ -390,6 +395,11 @@ func ByLastActiveAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldLastActiveAt, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByRestrictPublicGroups orders the results by the restrict_public_groups field.
|
||||
func ByRestrictPublicGroups(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldRestrictPublicGroups, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByBalanceNotifyEnabled orders the results by the balance_notify_enabled field.
|
||||
func ByBalanceNotifyEnabled(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldBalanceNotifyEnabled, opts...).ToFunc()
|
||||
|
||||
@@ -145,6 +145,11 @@ func LastActiveAt(v time.Time) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldLastActiveAt, v))
|
||||
}
|
||||
|
||||
// RestrictPublicGroups applies equality check predicate on the "restrict_public_groups" field. It's identical to RestrictPublicGroupsEQ.
|
||||
func RestrictPublicGroups(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldRestrictPublicGroups, v))
|
||||
}
|
||||
|
||||
// BalanceNotifyEnabled applies equality check predicate on the "balance_notify_enabled" field. It's identical to BalanceNotifyEnabledEQ.
|
||||
func BalanceNotifyEnabled(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldBalanceNotifyEnabled, v))
|
||||
@@ -1115,6 +1120,16 @@ func LastActiveAtNotNil() predicate.User {
|
||||
return predicate.User(sql.FieldNotNull(FieldLastActiveAt))
|
||||
}
|
||||
|
||||
// RestrictPublicGroupsEQ applies the EQ predicate on the "restrict_public_groups" field.
|
||||
func RestrictPublicGroupsEQ(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldRestrictPublicGroups, v))
|
||||
}
|
||||
|
||||
// RestrictPublicGroupsNEQ applies the NEQ predicate on the "restrict_public_groups" field.
|
||||
func RestrictPublicGroupsNEQ(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldNEQ(FieldRestrictPublicGroups, v))
|
||||
}
|
||||
|
||||
// BalanceNotifyEnabledEQ applies the EQ predicate on the "balance_notify_enabled" field.
|
||||
func BalanceNotifyEnabledEQ(v bool) predicate.User {
|
||||
return predicate.User(sql.FieldEQ(FieldBalanceNotifyEnabled, v))
|
||||
|
||||
@@ -270,6 +270,20 @@ func (_c *UserCreate) SetNillableLastActiveAt(v *time.Time) *UserCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
|
||||
func (_c *UserCreate) SetRestrictPublicGroups(v bool) *UserCreate {
|
||||
_c.mutation.SetRestrictPublicGroups(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableRestrictPublicGroups sets the "restrict_public_groups" field if the given value is not nil.
|
||||
func (_c *UserCreate) SetNillableRestrictPublicGroups(v *bool) *UserCreate {
|
||||
if v != nil {
|
||||
_c.SetRestrictPublicGroups(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (_c *UserCreate) SetBalanceNotifyEnabled(v bool) *UserCreate {
|
||||
_c.mutation.SetBalanceNotifyEnabled(v)
|
||||
@@ -636,6 +650,10 @@ func (_c *UserCreate) defaults() error {
|
||||
v := user.DefaultSignupSource
|
||||
_c.mutation.SetSignupSource(v)
|
||||
}
|
||||
if _, ok := _c.mutation.RestrictPublicGroups(); !ok {
|
||||
v := user.DefaultRestrictPublicGroups
|
||||
_c.mutation.SetRestrictPublicGroups(v)
|
||||
}
|
||||
if _, ok := _c.mutation.BalanceNotifyEnabled(); !ok {
|
||||
v := user.DefaultBalanceNotifyEnabled
|
||||
_c.mutation.SetBalanceNotifyEnabled(v)
|
||||
@@ -730,6 +748,9 @@ func (_c *UserCreate) check() error {
|
||||
return &ValidationError{Name: "signup_source", err: fmt.Errorf(`ent: validator failed for field "User.signup_source": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.RestrictPublicGroups(); !ok {
|
||||
return &ValidationError{Name: "restrict_public_groups", err: errors.New(`ent: missing required field "User.restrict_public_groups"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.BalanceNotifyEnabled(); !ok {
|
||||
return &ValidationError{Name: "balance_notify_enabled", err: errors.New(`ent: missing required field "User.balance_notify_enabled"`)}
|
||||
}
|
||||
@@ -844,6 +865,10 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(user.FieldLastActiveAt, field.TypeTime, value)
|
||||
_node.LastActiveAt = &value
|
||||
}
|
||||
if value, ok := _c.mutation.RestrictPublicGroups(); ok {
|
||||
_spec.SetField(user.FieldRestrictPublicGroups, field.TypeBool, value)
|
||||
_node.RestrictPublicGroups = value
|
||||
}
|
||||
if value, ok := _c.mutation.BalanceNotifyEnabled(); ok {
|
||||
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
|
||||
_node.BalanceNotifyEnabled = value
|
||||
@@ -1384,6 +1409,18 @@ func (u *UserUpsert) ClearLastActiveAt() *UserUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
|
||||
func (u *UserUpsert) SetRestrictPublicGroups(v bool) *UserUpsert {
|
||||
u.Set(user.FieldRestrictPublicGroups, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateRestrictPublicGroups sets the "restrict_public_groups" field to the value that was provided on create.
|
||||
func (u *UserUpsert) UpdateRestrictPublicGroups() *UserUpsert {
|
||||
u.SetExcluded(user.FieldRestrictPublicGroups)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (u *UserUpsert) SetBalanceNotifyEnabled(v bool) *UserUpsert {
|
||||
u.Set(user.FieldBalanceNotifyEnabled, v)
|
||||
@@ -1819,6 +1856,20 @@ func (u *UserUpsertOne) ClearLastActiveAt() *UserUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
|
||||
func (u *UserUpsertOne) SetRestrictPublicGroups(v bool) *UserUpsertOne {
|
||||
return u.Update(func(s *UserUpsert) {
|
||||
s.SetRestrictPublicGroups(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRestrictPublicGroups sets the "restrict_public_groups" field to the value that was provided on create.
|
||||
func (u *UserUpsertOne) UpdateRestrictPublicGroups() *UserUpsertOne {
|
||||
return u.Update(func(s *UserUpsert) {
|
||||
s.UpdateRestrictPublicGroups()
|
||||
})
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (u *UserUpsertOne) SetBalanceNotifyEnabled(v bool) *UserUpsertOne {
|
||||
return u.Update(func(s *UserUpsert) {
|
||||
@@ -2436,6 +2487,20 @@ func (u *UserUpsertBulk) ClearLastActiveAt() *UserUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
|
||||
func (u *UserUpsertBulk) SetRestrictPublicGroups(v bool) *UserUpsertBulk {
|
||||
return u.Update(func(s *UserUpsert) {
|
||||
s.SetRestrictPublicGroups(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRestrictPublicGroups sets the "restrict_public_groups" field to the value that was provided on create.
|
||||
func (u *UserUpsertBulk) UpdateRestrictPublicGroups() *UserUpsertBulk {
|
||||
return u.Update(func(s *UserUpsert) {
|
||||
s.UpdateRestrictPublicGroups()
|
||||
})
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (u *UserUpsertBulk) SetBalanceNotifyEnabled(v bool) *UserUpsertBulk {
|
||||
return u.Update(func(s *UserUpsert) {
|
||||
|
||||
@@ -321,6 +321,20 @@ func (_u *UserUpdate) ClearLastActiveAt() *UserUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
|
||||
func (_u *UserUpdate) SetRestrictPublicGroups(v bool) *UserUpdate {
|
||||
_u.mutation.SetRestrictPublicGroups(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableRestrictPublicGroups sets the "restrict_public_groups" field if the given value is not nil.
|
||||
func (_u *UserUpdate) SetNillableRestrictPublicGroups(v *bool) *UserUpdate {
|
||||
if v != nil {
|
||||
_u.SetRestrictPublicGroups(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (_u *UserUpdate) SetBalanceNotifyEnabled(v bool) *UserUpdate {
|
||||
_u.mutation.SetBalanceNotifyEnabled(v)
|
||||
@@ -1069,6 +1083,9 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.LastActiveAtCleared() {
|
||||
_spec.ClearField(user.FieldLastActiveAt, field.TypeTime)
|
||||
}
|
||||
if value, ok := _u.mutation.RestrictPublicGroups(); ok {
|
||||
_spec.SetField(user.FieldRestrictPublicGroups, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
|
||||
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
|
||||
}
|
||||
@@ -1997,6 +2014,20 @@ func (_u *UserUpdateOne) ClearLastActiveAt() *UserUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRestrictPublicGroups sets the "restrict_public_groups" field.
|
||||
func (_u *UserUpdateOne) SetRestrictPublicGroups(v bool) *UserUpdateOne {
|
||||
_u.mutation.SetRestrictPublicGroups(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableRestrictPublicGroups sets the "restrict_public_groups" field if the given value is not nil.
|
||||
func (_u *UserUpdateOne) SetNillableRestrictPublicGroups(v *bool) *UserUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetRestrictPublicGroups(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
|
||||
func (_u *UserUpdateOne) SetBalanceNotifyEnabled(v bool) *UserUpdateOne {
|
||||
_u.mutation.SetBalanceNotifyEnabled(v)
|
||||
@@ -2775,6 +2806,9 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
|
||||
if _u.mutation.LastActiveAtCleared() {
|
||||
_spec.ClearField(user.FieldLastActiveAt, field.TypeTime)
|
||||
}
|
||||
if value, ok := _u.mutation.RestrictPublicGroups(); ok {
|
||||
_spec.SetField(user.FieldRestrictPublicGroups, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
|
||||
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
|
||||
}
|
||||
|
||||
+17
-18
@@ -31,7 +31,7 @@ require (
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/redis/go-redis/v9 v9.22.0
|
||||
github.com/refraction-networking/utls v1.8.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.6
|
||||
@@ -50,13 +50,13 @@ require (
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||
github.com/zeromicro/go-zero v1.9.4
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/image v0.41.0
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/term v0.44.0
|
||||
google.golang.org/grpc v1.82.1
|
||||
golang.org/x/mod v0.38.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/term v0.45.0
|
||||
google.golang.org/grpc v1.83.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -101,7 +101,6 @@ require (
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.1.0 // indirect
|
||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
||||
@@ -135,7 +134,7 @@ require (
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/icholy/digest v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
@@ -193,20 +192,20 @@ require (
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
+42
-43
@@ -159,8 +159,6 @@ github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dr
|
||||
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2/v2 v2.1.0 h1:jHXRmHRZGbuQzDZjMlCAXOvQb75iv3HyLDzXGj5H1AY=
|
||||
@@ -300,8 +298,8 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCy
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -393,8 +391,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
|
||||
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@@ -511,30 +509,32 @@ github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8
|
||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
github.com/zeromicro/go-zero v1.9.4 h1:aRLFoISqAYijABtkbliQC5SsI5TbizJpQvoHc9xup8k=
|
||||
github.com/zeromicro/go-zero v1.9.4/go.mod h1:a17JOTch25SWxBcUgJZYps60hygK3pIYdw7nGwlcS38=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -562,8 +562,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
@@ -578,8 +578,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -601,8 +601,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -614,8 +614,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -633,7 +633,6 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -648,8 +647,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -662,8 +661,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -674,8 +673,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -690,8 +689,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -702,16 +701,16 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
@@ -89,6 +89,7 @@ type Config struct {
|
||||
Pricing PricingConfig `mapstructure:"pricing"`
|
||||
Gateway GatewayConfig `mapstructure:"gateway"`
|
||||
APIKeyAuth APIKeyAuthCacheConfig `mapstructure:"api_key_auth_cache"`
|
||||
APIKeyCreate APIKeyCreateConfig `mapstructure:"api_key_create"`
|
||||
SubscriptionCache SubscriptionCacheConfig `mapstructure:"subscription_cache"`
|
||||
SubscriptionMaintenance SubscriptionMaintenanceConfig `mapstructure:"subscription_maintenance"`
|
||||
Dashboard DashboardCacheConfig `mapstructure:"dashboard_cache"`
|
||||
@@ -96,6 +97,7 @@ type Config struct {
|
||||
UsageCleanup UsageCleanupConfig `mapstructure:"usage_cleanup"`
|
||||
Concurrency ConcurrencyConfig `mapstructure:"concurrency"`
|
||||
TokenRefresh TokenRefreshConfig `mapstructure:"token_refresh"`
|
||||
SimpleMode SimpleModeConfig `mapstructure:"simple_mode" yaml:"simple_mode"`
|
||||
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
||||
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
||||
Gemini GeminiConfig `mapstructure:"gemini"`
|
||||
@@ -104,6 +106,14 @@ type Config struct {
|
||||
BatchImage BatchImageConfig `mapstructure:"batch_image"`
|
||||
ImageStorage ImageStorageConfig `mapstructure:"image_storage"`
|
||||
Plugins PluginConfig `mapstructure:"plugins"`
|
||||
|
||||
// Enforce only API-key spending windows in simple mode.
|
||||
SimpleModeKeyRateLimitEnabled bool `mapstructure:"simple_mode_key_rate_limit_enabled" yaml:"simple_mode_key_rate_limit_enabled"`
|
||||
}
|
||||
|
||||
// SimpleModeConfig controls startup behavior in simple mode.
|
||||
type SimpleModeConfig struct {
|
||||
AutoCreateDefaultGroups bool `mapstructure:"auto_create_default_groups" yaml:"auto_create_default_groups"`
|
||||
}
|
||||
|
||||
// PluginConfig 控制管理员手动上传的本地进程插件。
|
||||
@@ -667,6 +677,8 @@ type PricingConfig struct {
|
||||
DataDir string `mapstructure:"data_dir"`
|
||||
// 回退文件路径
|
||||
FallbackFile string `mapstructure:"fallback_file"`
|
||||
// 覆盖补丁文件路径(可选):条目按字段浅合并覆盖目录/回退数据,优先级最高
|
||||
OverrideFile string `mapstructure:"override_file"`
|
||||
// 更新间隔(小时)
|
||||
UpdateIntervalHours int `mapstructure:"update_interval_hours"`
|
||||
// 哈希校验间隔(分钟)
|
||||
@@ -910,6 +922,29 @@ type BillingConfig struct {
|
||||
// UserPlatformQuotaSentinelTTLSeconds sentinel(无 limit 占位)entry 的 TTL,
|
||||
// 显著短于 quota cache 默认 86400s 以控 Redis 内存;默认 3600=1h。
|
||||
UserPlatformQuotaSentinelTTLSeconds int `mapstructure:"user_platform_quota_sentinel_ttl_seconds"`
|
||||
// InflightReservation 余额模式在途请求预留(Redis),防止并发请求在预检时看到同一份余额而集体透支。
|
||||
InflightReservation InflightReservationConfig `mapstructure:"inflight_reservation"`
|
||||
}
|
||||
|
||||
// InflightReservationConfig 余额模式在途预留配置。
|
||||
// 准入时按 输入估算 + 输出单价 × max_tokens 估算单请求费用,在 Redis 中原子地
|
||||
// 校验 缓存余额 - 在途预留合计 >= 估算 后登记预留,请求结束(任意路径)释放。
|
||||
// 估算失败或 Redis 不可用时 fail-open,退回旧的仅余额 > 阈值检查。
|
||||
type InflightReservationConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
// TTLSeconds 单条预留的最长存活时间;进程崩溃等泄漏的预留到期自动失效。
|
||||
TTLSeconds int `mapstructure:"ttl_seconds"`
|
||||
// DefaultMaxTokens 请求未携带 max_tokens 时用于估算的输出 token 数。
|
||||
DefaultMaxTokens int `mapstructure:"default_max_tokens"`
|
||||
// MaxOutputTokens 估算输出 token 的上限(max_tokens 超出时截断)。
|
||||
MaxOutputTokens int `mapstructure:"max_output_tokens"`
|
||||
// MaxInputTokens 输入 token 估算(请求体字节数 / 4)的上限。
|
||||
MaxInputTokens int `mapstructure:"max_input_tokens"`
|
||||
// MaxReservationUSD 单请求预留金额上限;0 表示不设上限。
|
||||
MaxReservationUSD float64 `mapstructure:"max_reservation_usd"`
|
||||
// FailClosedOnUnpriced 无法为请求估算费用(模型/分组/渠道均无定价)时是否拒绝请求。
|
||||
// 默认 false:放行且不预留(fail-open,节流告警日志)。
|
||||
FailClosedOnUnpriced bool `mapstructure:"fail_closed_on_unpriced"`
|
||||
}
|
||||
|
||||
type CircuitBreakerConfig struct {
|
||||
@@ -1272,11 +1307,15 @@ type GatewayOpenAIWSConfig struct {
|
||||
MaxConnsPerAccount int `mapstructure:"max_conns_per_account"`
|
||||
MinIdlePerAccount int `mapstructure:"min_idle_per_account"`
|
||||
MaxIdlePerAccount int `mapstructure:"max_idle_per_account"`
|
||||
// DynamicMaxConnsByAccountConcurrencyEnabled: 是否按账号并发动态计算连接池上限
|
||||
// DynamicMaxConnsByAccountConcurrencyEnabled: 是否按账号并发动态计算连接池上限。
|
||||
// 旧版及 mode_router_v2 的 ctx_pool 共用此开关和类型系数;关闭后使用 max_conns_per_account。
|
||||
// mode_router_v2 下并发数 <= 0 的账号仍不可调度。
|
||||
DynamicMaxConnsByAccountConcurrencyEnabled bool `mapstructure:"dynamic_max_conns_by_account_concurrency_enabled"`
|
||||
// OAuthMaxConnsFactor: OAuth 账号连接池系数(effective=ceil(concurrency*factor))
|
||||
// OAuthMaxConnsFactor: OAuth 账号连接池系数(effective=ceil(concurrency*factor),再受 max_conns_per_account 封顶)。
|
||||
// ctx_pool 接入下每个客户端会话在整个生命周期(含轮次之间)持有一条上游连接,此上限限制的是同时持有连接的会话数,
|
||||
// 在飞请求数另由账号并发槽限制;系数 1.0 会让存活会话数一到并发数就返回 1013 busy,默认 5.0。
|
||||
OAuthMaxConnsFactor float64 `mapstructure:"oauth_max_conns_factor"`
|
||||
// APIKeyMaxConnsFactor: API Key 账号连接池系数(effective=ceil(concurrency*factor))
|
||||
// APIKeyMaxConnsFactor: API Key 账号连接池系数,含义与 OAuthMaxConnsFactor 相同,默认 5.0。
|
||||
APIKeyMaxConnsFactor float64 `mapstructure:"apikey_max_conns_factor"`
|
||||
DialTimeoutSeconds int `mapstructure:"dial_timeout_seconds"`
|
||||
ReadTimeoutSeconds int `mapstructure:"read_timeout_seconds"`
|
||||
@@ -1611,10 +1650,10 @@ type OpsCleanupConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Schedule string `mapstructure:"schedule"`
|
||||
|
||||
// Retention days (0 disables that cleanup target).
|
||||
//
|
||||
// vNext requirement: default 30 days across ops datasets.
|
||||
// Retention days. Error and metrics targets accept 0 as an explicit truncate;
|
||||
// system logs require a positive value because their runtime setting is bounded.
|
||||
ErrorLogRetentionDays int `mapstructure:"error_log_retention_days"`
|
||||
SystemLogRetentionDays int `mapstructure:"system_log_retention_days"`
|
||||
MinuteMetricsRetentionDays int `mapstructure:"minute_metrics_retention_days"`
|
||||
HourlyMetricsRetentionDays int `mapstructure:"hourly_metrics_retention_days"`
|
||||
}
|
||||
@@ -1681,6 +1720,14 @@ type APIKeyAuthCacheConfig struct {
|
||||
InvalidAbuse InvalidAuthAbuseConfig `mapstructure:"invalid_abuse"`
|
||||
}
|
||||
|
||||
// APIKeyCreateConfig 用户创建 API Key 的防滥用限制(0 表示不限制)
|
||||
type APIKeyCreateConfig struct {
|
||||
// MaxActivePerUser 单个用户同时存在(未删除)的 API Key 上限
|
||||
MaxActivePerUser int `mapstructure:"max_active_per_user"`
|
||||
// MaxPerUserPerHour 单个用户每小时可创建的 API Key 次数(删除不返还次数)
|
||||
MaxPerUserPerHour int `mapstructure:"max_per_user_per_hour"`
|
||||
}
|
||||
|
||||
type InvalidAuthAbuseConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Threshold int `mapstructure:"threshold"`
|
||||
@@ -1986,6 +2033,8 @@ func configureConfigSource(setConfigFile, addConfigPath func(string)) {
|
||||
|
||||
func setDefaults() {
|
||||
viper.SetDefault("run_mode", RunModeStandard)
|
||||
viper.SetDefault("simple_mode.auto_create_default_groups", true)
|
||||
viper.SetDefault("simple_mode_key_rate_limit_enabled", false)
|
||||
|
||||
// Server
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
@@ -2044,7 +2093,9 @@ func setDefaults() {
|
||||
"api.moonshot.ai",
|
||||
"api.moonshot.cn",
|
||||
"open.bigmodel.cn",
|
||||
"api.minimaxi.com",
|
||||
"api.minimaxi.com", // MiniMax CN quota + inference
|
||||
"api.minimax.io", // MiniMax intl; frozen allowlists must add this host to use the intl site
|
||||
"opencode.ai",
|
||||
"generativelanguage.googleapis.com",
|
||||
"cloudcode-pa.googleapis.com",
|
||||
"*.openai.azure.com",
|
||||
@@ -2074,6 +2125,13 @@ func setDefaults() {
|
||||
viper.SetDefault("billing.minimum_balance_reserve", 0.000001)
|
||||
viper.SetDefault("billing.user_platform_quota_cache_ttl_seconds", 86400)
|
||||
viper.SetDefault("billing.user_platform_quota_sentinel_ttl_seconds", 3600)
|
||||
viper.SetDefault("billing.inflight_reservation.enabled", true)
|
||||
viper.SetDefault("billing.inflight_reservation.ttl_seconds", 900)
|
||||
viper.SetDefault("billing.inflight_reservation.default_max_tokens", 8192)
|
||||
viper.SetDefault("billing.inflight_reservation.max_output_tokens", 128000)
|
||||
viper.SetDefault("billing.inflight_reservation.max_input_tokens", 200000)
|
||||
viper.SetDefault("billing.inflight_reservation.max_reservation_usd", 0)
|
||||
viper.SetDefault("billing.inflight_reservation.fail_closed_on_unpriced", false)
|
||||
|
||||
// Turnstile
|
||||
viper.SetDefault("turnstile.required", false)
|
||||
@@ -2248,6 +2306,7 @@ func setDefaults() {
|
||||
viper.SetDefault("ops.cleanup.schedule", "0 2 * * *")
|
||||
// Retention days: vNext defaults to 30 days across ops datasets.
|
||||
viper.SetDefault("ops.cleanup.error_log_retention_days", 30)
|
||||
viper.SetDefault("ops.cleanup.system_log_retention_days", 30)
|
||||
viper.SetDefault("ops.cleanup.minute_metrics_retention_days", 30)
|
||||
viper.SetDefault("ops.cleanup.hourly_metrics_retention_days", 30)
|
||||
viper.SetDefault("ops.aggregation.enabled", true)
|
||||
@@ -2279,11 +2338,12 @@ func setDefaults() {
|
||||
viper.SetDefault("rate_limit.overload_cooldown_minutes", 10)
|
||||
viper.SetDefault("rate_limit.oauth_401_cooldown_minutes", 10)
|
||||
|
||||
// Pricing - 从 model-price-repo 同步模型定价和上下文窗口数据(固定到 commit,避免分支漂移)
|
||||
// Pricing - 从 model-price-repo main 分支同步模型定价和上下文窗口数据
|
||||
viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.json")
|
||||
viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.sha256")
|
||||
viper.SetDefault("pricing.data_dir", "./data")
|
||||
viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json")
|
||||
viper.SetDefault("pricing.override_file", "")
|
||||
viper.SetDefault("pricing.update_interval_hours", 24)
|
||||
viper.SetDefault("pricing.hash_check_interval_minutes", 10)
|
||||
|
||||
@@ -2311,6 +2371,8 @@ func setDefaults() {
|
||||
viper.SetDefault("api_key_auth_cache.invalid_abuse.window_seconds", 60)
|
||||
viper.SetDefault("api_key_auth_cache.invalid_abuse.block_seconds", 60)
|
||||
viper.SetDefault("api_key_auth_cache.invalid_abuse.capacity", 16384)
|
||||
viper.SetDefault("api_key_create.max_active_per_user", 200)
|
||||
viper.SetDefault("api_key_create.max_per_user_per_hour", 60)
|
||||
|
||||
// Subscription auth L1 cache
|
||||
viper.SetDefault("subscription_cache.l1_size", 16384)
|
||||
@@ -2370,7 +2432,7 @@ func setDefaults() {
|
||||
viper.SetDefault("gateway.disable_codex_originator_normalization", false)
|
||||
viper.SetDefault("gateway.codex_image_generation_bridge_enabled", false)
|
||||
viper.SetDefault("gateway.openai_passthrough_allow_timeout_headers", false)
|
||||
viper.SetDefault("gateway.openai_compact_model", "gpt-5.4")
|
||||
viper.SetDefault("gateway.openai_compact_model", "gpt-5.5")
|
||||
viper.SetDefault("gateway.live.max_session_duration_seconds", 3600)
|
||||
// OpenAI Responses WebSocket(默认开启;可通过 force_http 紧急回滚)
|
||||
viper.SetDefault("gateway.openai_ws.enabled", true)
|
||||
@@ -2396,8 +2458,8 @@ func setDefaults() {
|
||||
viper.SetDefault("gateway.openai_ws.min_idle_per_account", 4)
|
||||
viper.SetDefault("gateway.openai_ws.max_idle_per_account", 12)
|
||||
viper.SetDefault("gateway.openai_ws.dynamic_max_conns_by_account_concurrency_enabled", true)
|
||||
viper.SetDefault("gateway.openai_ws.oauth_max_conns_factor", 1.0)
|
||||
viper.SetDefault("gateway.openai_ws.apikey_max_conns_factor", 1.0)
|
||||
viper.SetDefault("gateway.openai_ws.oauth_max_conns_factor", 5.0)
|
||||
viper.SetDefault("gateway.openai_ws.apikey_max_conns_factor", 5.0)
|
||||
viper.SetDefault("gateway.openai_ws.dial_timeout_seconds", 10)
|
||||
viper.SetDefault("gateway.openai_ws.read_timeout_seconds", 900)
|
||||
viper.SetDefault("gateway.openai_ws.write_timeout_seconds", 120)
|
||||
@@ -2690,6 +2752,12 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("server.h2c.max_upload_buffer_per_stream must be positive")
|
||||
}
|
||||
}
|
||||
if c.APIKeyCreate.MaxActivePerUser < 0 {
|
||||
return fmt.Errorf("api_key_create.max_active_per_user must be non-negative")
|
||||
}
|
||||
if c.APIKeyCreate.MaxPerUserPerHour < 0 {
|
||||
return fmt.Errorf("api_key_create.max_per_user_per_hour must be non-negative")
|
||||
}
|
||||
if c.APIKeyAuth.InvalidAbuse.Enabled {
|
||||
if c.APIKeyAuth.InvalidAbuse.Threshold < 10 {
|
||||
return fmt.Errorf("api_key_auth_cache.invalid_abuse.threshold must be at least 10")
|
||||
@@ -3041,6 +3109,11 @@ func (c *Config) Validate() error {
|
||||
if c.Billing.MinimumBalanceReserve < 0 {
|
||||
return fmt.Errorf("billing.minimum_balance_reserve must be non-negative")
|
||||
}
|
||||
if c.Billing.InflightReservation.TTLSeconds < 0 || c.Billing.InflightReservation.DefaultMaxTokens < 0 ||
|
||||
c.Billing.InflightReservation.MaxOutputTokens < 0 || c.Billing.InflightReservation.MaxInputTokens < 0 ||
|
||||
c.Billing.InflightReservation.MaxReservationUSD < 0 {
|
||||
return fmt.Errorf("billing.inflight_reservation values must be non-negative")
|
||||
}
|
||||
if c.Database.MaxOpenConns <= 0 {
|
||||
return fmt.Errorf("database.max_open_conns must be positive")
|
||||
}
|
||||
@@ -3670,6 +3743,9 @@ func (c *Config) Validate() error {
|
||||
if c.Ops.Cleanup.ErrorLogRetentionDays < 0 {
|
||||
return fmt.Errorf("ops.cleanup.error_log_retention_days must be non-negative")
|
||||
}
|
||||
if c.Ops.Cleanup.Enabled && c.Ops.Cleanup.SystemLogRetentionDays <= 0 {
|
||||
return fmt.Errorf("ops.cleanup.system_log_retention_days must be positive when ops cleanup is enabled")
|
||||
}
|
||||
if c.Ops.Cleanup.MinuteMetricsRetentionDays < 0 {
|
||||
return fmt.Errorf("ops.cleanup.minute_metrics_retention_days must be non-negative")
|
||||
}
|
||||
|
||||
@@ -79,6 +79,15 @@ func TestLoadServerTimingConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadSimpleModeKeyRateLimitEnabledFromEnvironment(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("SIMPLE_MODE_KEY_RATE_LIMIT_ENABLED", "true")
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.True(t, cfg.SimpleModeKeyRateLimitEnabled)
|
||||
}
|
||||
|
||||
func TestLoadRedisUsernameFromEnvironment(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("REDIS_USERNAME", "app-user")
|
||||
@@ -452,11 +461,11 @@ func TestLoadDefaultOpenAIWSConfig(t *testing.T) {
|
||||
if !cfg.Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled {
|
||||
t.Fatalf("Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled = false, want true")
|
||||
}
|
||||
if cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor != 1.0 {
|
||||
t.Fatalf("Gateway.OpenAIWS.OAuthMaxConnsFactor = %v, want 1.0", cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor)
|
||||
if cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor != 5.0 {
|
||||
t.Fatalf("Gateway.OpenAIWS.OAuthMaxConnsFactor = %v, want 5.0", cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor)
|
||||
}
|
||||
if cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor != 1.0 {
|
||||
t.Fatalf("Gateway.OpenAIWS.APIKeyMaxConnsFactor = %v, want 1.0", cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor)
|
||||
if cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor != 5.0 {
|
||||
t.Fatalf("Gateway.OpenAIWS.APIKeyMaxConnsFactor = %v, want 5.0", cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor)
|
||||
}
|
||||
if cfg.Gateway.OpenAIWS.StickySessionTTLSeconds != 3600 {
|
||||
t.Fatalf("Gateway.OpenAIWS.StickySessionTTLSeconds = %d, want 3600", cfg.Gateway.OpenAIWS.StickySessionTTLSeconds)
|
||||
@@ -574,7 +583,7 @@ func TestLoadDefaultOpenAICompactModel(t *testing.T) {
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "gpt-5.4", cfg.Gateway.OpenAICompactModel)
|
||||
require.Equal(t, "gpt-5.5", cfg.Gateway.OpenAICompactModel)
|
||||
}
|
||||
|
||||
func TestLoadOpenAICompactModelFromEnv(t *testing.T) {
|
||||
@@ -1191,6 +1200,21 @@ func TestLoadDefaultUsageCleanupConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaultOpsCleanupConfig(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
if !cfg.Ops.Cleanup.Enabled {
|
||||
t.Fatal("Ops.Cleanup.Enabled = false, want true")
|
||||
}
|
||||
if cfg.Ops.Cleanup.SystemLogRetentionDays != 30 {
|
||||
t.Fatalf("Ops.Cleanup.SystemLogRetentionDays = %d, want 30", cfg.Ops.Cleanup.SystemLogRetentionDays)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUsageCleanupConfigEnabled(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
@@ -2137,6 +2161,11 @@ func TestValidateConfigErrors(t *testing.T) {
|
||||
mutate: func(c *Config) { c.Ops.Cleanup.ErrorLogRetentionDays = -1 },
|
||||
wantErr: "ops.cleanup.error_log_retention_days",
|
||||
},
|
||||
{
|
||||
name: "ops cleanup system log retention",
|
||||
mutate: func(c *Config) { c.Ops.Cleanup.SystemLogRetentionDays = 0 },
|
||||
wantErr: "ops.cleanup.system_log_retention_days",
|
||||
},
|
||||
{
|
||||
name: "ops cleanup minute retention",
|
||||
mutate: func(c *Config) { c.Ops.Cleanup.MinuteMetricsRetentionDays = -1 },
|
||||
@@ -2610,3 +2639,42 @@ func TestLoad_DefaultGatewayImageStreamConfig(t *testing.T) {
|
||||
t.Fatalf("image stream timeout = %d, want greater than ordinary stream timeout %d", cfg.Gateway.ImageStreamDataIntervalTimeout, cfg.Gateway.StreamDataIntervalTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSimpleModeAutoCreateDefaultGroups(t *testing.T) {
|
||||
for _, loader := range []struct {
|
||||
name string
|
||||
load func() (*Config, error)
|
||||
}{{"Load", Load}, {"LoadForBootstrap", LoadForBootstrap}} {
|
||||
t.Run(loader.name, func(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
yaml string
|
||||
env string
|
||||
unset bool
|
||||
want bool
|
||||
}{
|
||||
{name: "unset env uses application default", unset: true, want: true},
|
||||
{name: "empty env uses application default", want: true},
|
||||
{name: "unset env preserves yaml false", unset: true, yaml: "simple_mode:\n auto_create_default_groups: false\n", want: false},
|
||||
{name: "empty env preserves yaml false", yaml: "simple_mode:\n auto_create_default_groups: false\n", want: false},
|
||||
{name: "env false overrides yaml true", yaml: "simple_mode:\n auto_create_default_groups: true\n", env: "false", want: false},
|
||||
{name: "env false without yaml", env: "false", want: false},
|
||||
{name: "env true overrides yaml false", yaml: "simple_mode:\n auto_create_default_groups: false\n", env: "true", want: true},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("SIMPLE_MODE_AUTO_CREATE_DEFAULT_GROUPS", tt.env)
|
||||
if tt.unset {
|
||||
require.NoError(t, os.Unsetenv("SIMPLE_MODE_AUTO_CREATE_DEFAULT_GROUPS"))
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("run_mode: simple\n"+tt.yaml), 0o600))
|
||||
t.Setenv("CONFIG_FILE", path)
|
||||
cfg, err := loader.load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, cfg.SimpleMode.AutoCreateDefaultGroups)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ import "time"
|
||||
// - "7d" 7 天窗口(Claude/Codex)
|
||||
// - "7d-sonnet" Claude 7 天 Sonnet 独立额度
|
||||
// - "7d-fable" Claude 7 天 Fable 独立额度
|
||||
// - "weekly" 周窗口(Kimi/Zhipu coding plan)
|
||||
// - "weekly" 周窗口(Kimi/Zhipu coding plan / OpenCode Go)
|
||||
// - "monthly" 月窗口(OpenCode Go)
|
||||
// - "daily" 日窗口(Gemini 日配额 / Grok 日请求)
|
||||
// - "30d" 30 天窗口(Grok 月度)
|
||||
// - "total" 无窗口语义的总量额度(Antigravity per-model 等)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
// GroupCodexModelsManifestConfig controls which accounts fetch the Codex
|
||||
// models manifest for an OpenAI group. When enabled, the group's Codex
|
||||
// /models requests bypass the scheduler and only use the pinned accounts.
|
||||
type GroupCodexModelsManifestConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
// AccountIDs is the ordered list of pinned account IDs. Order decides
|
||||
// merge precedence for duplicate model slugs.
|
||||
AccountIDs []int64 `json:"account_ids,omitempty"`
|
||||
// FallbackToScheduler controls the behavior when no pinned account is
|
||||
// usable or all upstream fetches fail: fall back to the scheduler loop
|
||||
// (true) or return the upstream error / 503 (false, default).
|
||||
FallbackToScheduler bool `json:"fallback_to_scheduler,omitempty"`
|
||||
}
|
||||
@@ -24,10 +24,15 @@ const (
|
||||
PlatformAntigravity = "antigravity"
|
||||
PlatformGrok = "grok"
|
||||
// 国产 OpenAI 兼容供应商(经 OpenAI 网关转发,按 Chat Completions 协议)。
|
||||
PlatformKimi = "kimi" // Kimi (月之暗面 / Moonshot)
|
||||
PlatformZhipu = "zhipu" // 智谱 GLM (bigmodel)
|
||||
PlatformDeepseek = "deepseek" // DeepSeek
|
||||
PlatformComposite = "composite"
|
||||
PlatformKimi = "kimi" // Kimi (月之暗面 / Moonshot)
|
||||
PlatformZhipu = "zhipu" // 智谱 GLM (bigmodel)
|
||||
PlatformDeepseek = "deepseek" // DeepSeek
|
||||
PlatformMiniMax = "minimax" // MiniMax (M 系列)
|
||||
PlatformTypeSafe = "typesafe" // TypeSafe AI System One (Jev)
|
||||
// PlatformOpenCodeGo 是 OpenCode 平台(账号类型 Zen 按量 / Go 订阅)。
|
||||
// 值保持 opencode_go 以兼容已落库的分组、配额与 Composite 路由 CHECK。
|
||||
PlatformOpenCodeGo = "opencode_go"
|
||||
PlatformComposite = "composite"
|
||||
)
|
||||
|
||||
// Account mode constants 区分国产供应商的「按量付费(余额)」与「Coding Plan」两种接入方式。
|
||||
@@ -35,6 +40,8 @@ const (
|
||||
const (
|
||||
AccountModePayG = "payg" // 按量付费:消耗余额,做余额检测冷却
|
||||
AccountModeCoding = "coding" // Coding Plan:滚动用量窗口冷却(5h / weekly)
|
||||
AccountModeZen = "zen" // OpenCode Zen:按量付费,https://opencode.ai/zen/v1
|
||||
AccountModeGo = "go" // OpenCode Go:订阅额度窗口,https://opencode.ai/zen/go/v1
|
||||
)
|
||||
|
||||
// API protocol constants 国产供应商的上游 API 协议维度。存储于
|
||||
@@ -43,7 +50,7 @@ const (
|
||||
const (
|
||||
APIProtocolChatCompletions = "chat_completions" // OpenAI Chat Completions(默认)
|
||||
APIProtocolAnthropic = "anthropic" // 原生 Anthropic /v1/messages(适配 Claude Code)
|
||||
APIProtocolResponses = "responses" // OpenAI Responses(仅 deepseek,适配 Codex)
|
||||
APIProtocolResponses = "responses" // OpenAI Responses(deepseek / kimi / minimax 原生端点,适配 Codex)
|
||||
APIProtocolAdaptive = "adaptive" // 按入站协议优先选择供应商原生端点
|
||||
)
|
||||
|
||||
@@ -98,6 +105,7 @@ const AntigravityGemini31ProAgentModel = "gemini-pro-agent"
|
||||
// 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致
|
||||
var DefaultAntigravityModelMapping = map[string]string{
|
||||
// Claude 白名单
|
||||
"claude-fable-5-1": "claude-fable-5-1", // 官方模型
|
||||
"claude-fable-5": "claude-fable-5", // 官方模型
|
||||
"claude-opus-4-8": "claude-opus-4-8", // 官方模型
|
||||
"claude-opus-4-7": "claude-opus-4-7", // 官方模型
|
||||
@@ -144,6 +152,18 @@ var DefaultAntigravityModelMapping = map[string]string{
|
||||
"gemini-3.6-flash-low": "gemini-3.6-flash-low",
|
||||
"gemini-3.6-flash-medium": "gemini-3.6-flash-medium",
|
||||
"gemini-3.6-flash-tiered": "gemini-3.6-flash-tiered",
|
||||
// Gemini 3.7 Flash tiered models
|
||||
"gemini-3.7-flash": "gemini-3.7-flash",
|
||||
"gemini-3.7-flash-high": "gemini-3.7-flash-high",
|
||||
"gemini-3.7-flash-low": "gemini-3.7-flash-low",
|
||||
"gemini-3.7-flash-medium": "gemini-3.7-flash-medium",
|
||||
"gemini-3.7-flash-tiered": "gemini-3.7-flash-tiered",
|
||||
// Gemini 3.8 Flash tiered models
|
||||
"gemini-3.8-flash": "gemini-3.8-flash",
|
||||
"gemini-3.8-flash-high": "gemini-3.8-flash-high",
|
||||
"gemini-3.8-flash-low": "gemini-3.8-flash-low",
|
||||
"gemini-3.8-flash-medium": "gemini-3.8-flash-medium",
|
||||
"gemini-3.8-flash-tiered": "gemini-3.8-flash-tiered",
|
||||
// Gemini 3 image 兼容映射(向 3.1 image 迁移)
|
||||
"gemini-3-pro-image": "gemini-3.1-flash-image",
|
||||
"gemini-3-pro-image-preview": "gemini-3.1-flash-image",
|
||||
@@ -158,7 +178,8 @@ var DefaultAntigravityModelMapping = map[string]string{
|
||||
// aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等)
|
||||
var DefaultBedrockModelMapping = map[string]string{
|
||||
// Claude Fable
|
||||
"claude-fable-5": "anthropic.claude-fable-5",
|
||||
"claude-fable-5-1": "anthropic.claude-fable-5-1",
|
||||
"claude-fable-5": "anthropic.claude-fable-5",
|
||||
// Claude Opus
|
||||
"claude-opus-5": "us.anthropic.claude-opus-5-v1",
|
||||
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
|
||||
@@ -170,6 +191,8 @@ var DefaultBedrockModelMapping = map[string]string{
|
||||
"claude-opus-4-1": "us.anthropic.claude-opus-4-1-20250805-v1:0",
|
||||
"claude-opus-4-20250514": "us.anthropic.claude-opus-4-20250514-v1:0",
|
||||
// Claude Sonnet
|
||||
// Sonnet 5.5 is available on bedrock-runtime through Global inference only.
|
||||
"claude-sonnet-5-5": "global.anthropic.claude-sonnet-5-5",
|
||||
"claude-sonnet-5": "us.anthropic.claude-sonnet-5-v1",
|
||||
"claude-sonnet-4-6-thinking": "us.anthropic.claude-sonnet-4-6",
|
||||
"claude-sonnet-4-6": "us.anthropic.claude-sonnet-4-6",
|
||||
|
||||
@@ -29,8 +29,9 @@ func TestDefaultAntigravityModelMapping_ContainsNewClaudeModels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]string{
|
||||
"claude-fable-5": "claude-fable-5",
|
||||
"claude-opus-4-8": "claude-opus-4-8",
|
||||
"claude-fable-5-1": "claude-fable-5-1",
|
||||
"claude-fable-5": "claude-fable-5",
|
||||
"claude-opus-4-8": "claude-opus-4-8",
|
||||
}
|
||||
for from, want := range cases {
|
||||
got, ok := DefaultAntigravityModelMapping[from]
|
||||
@@ -88,12 +89,30 @@ func TestDefaultAntigravityModelMapping_Gemini36FlashModels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAntigravityModelMapping_Gemini37FlashModels(t *testing.T) {
|
||||
for _, model := range []string{"gemini-3.7-flash", "gemini-3.7-flash-high", "gemini-3.7-flash-low", "gemini-3.7-flash-medium", "gemini-3.7-flash-tiered"} {
|
||||
if got := DefaultAntigravityModelMapping[model]; got != model {
|
||||
t.Fatalf("expected %s to map to itself, got %q", model, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAntigravityModelMapping_Gemini38FlashModels(t *testing.T) {
|
||||
for _, model := range []string{"gemini-3.8-flash", "gemini-3.8-flash-high", "gemini-3.8-flash-low", "gemini-3.8-flash-medium", "gemini-3.8-flash-tiered"} {
|
||||
if got := DefaultAntigravityModelMapping[model]; got != model {
|
||||
t.Fatalf("expected %s to map to itself, got %q", model, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultBedrockModelMapping_ContainsNewClaudeModels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]string{
|
||||
"claude-fable-5": "anthropic.claude-fable-5",
|
||||
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
|
||||
"claude-fable-5-1": "anthropic.claude-fable-5-1",
|
||||
"claude-fable-5": "anthropic.claude-fable-5",
|
||||
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
|
||||
"claude-sonnet-5-5": "global.anthropic.claude-sonnet-5-5",
|
||||
}
|
||||
for from, want := range cases {
|
||||
got, ok := DefaultBedrockModelMapping[from]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package domain
|
||||
|
||||
// GroupModelAllowlist 是分组级模型白名单:开启后既过滤模型列表类接口的返回内容,
|
||||
// 也约束分组实际可调用的模型(网关准入在合成路由改写与调度之前完成)。
|
||||
type GroupModelAllowlist struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Models []string `json:"models,omitempty"`
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package domain
|
||||
|
||||
// GroupModelsListConfig controls the optional custom /v1/models response list.
|
||||
type GroupModelsListConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Models []string `json:"models,omitempty"`
|
||||
}
|
||||
@@ -1,8 +1,21 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
ReasoningEffortMatchExact = "exact"
|
||||
ReasoningEffortMatchPrefix = "prefix"
|
||||
ReasoningEffortMatchSuffix = "suffix"
|
||||
)
|
||||
|
||||
// ReasoningEffortMapping rewrites one explicit OpenAI/Codex reasoning effort
|
||||
// value to another before the group ceiling is applied.
|
||||
// value to another before the group ceiling is applied. To may also be "deny"
|
||||
// to reject the request when the matching source value is present.
|
||||
//
|
||||
// Model and MatchType optionally scope the rewrite to a request model:
|
||||
// exact matches the full model id, prefix/suffix match a model-id affix.
|
||||
// Empty MatchType and empty Model mean the mapping applies to every model.
|
||||
type ReasoningEffortMapping struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
MatchType string `json:"match_type,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
@@ -309,15 +309,17 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
||||
_, _ = h.adminService.UpdateProxy(ctx, existingID, &service.UpdateProxyInput{
|
||||
Status: normalizedStatus,
|
||||
ExpiresAt: existingExpiresAt,
|
||||
ClearExpiresAt: existingExpiresAt == nil,
|
||||
FallbackMode: existingFallbackMode,
|
||||
BackupProxyID: existingBackupProxyID,
|
||||
ExpiryWarnDays: item.ExpiryWarnDays,
|
||||
ClearBackupID: existingBackupProxyID == nil,
|
||||
ExpiryWarnDays: &item.ExpiryWarnDays,
|
||||
Name: proxy.Name,
|
||||
Protocol: proxy.Protocol,
|
||||
Host: proxy.Host,
|
||||
Port: proxy.Port,
|
||||
Username: proxy.Username,
|
||||
Password: proxy.Password,
|
||||
Username: &proxy.Username,
|
||||
Password: &proxy.Password,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -383,15 +385,17 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
||||
_, _ = h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{
|
||||
Status: normalizedStatus,
|
||||
ExpiresAt: expiresAt,
|
||||
ClearExpiresAt: expiresAt == nil,
|
||||
FallbackMode: fallbackMode,
|
||||
BackupProxyID: backupProxyID,
|
||||
ExpiryWarnDays: item.ExpiryWarnDays,
|
||||
ClearBackupID: backupProxyID == nil,
|
||||
ExpiryWarnDays: &item.ExpiryWarnDays,
|
||||
Name: created.Name,
|
||||
Protocol: created.Protocol,
|
||||
Host: created.Host,
|
||||
Port: created.Port,
|
||||
Username: created.Username,
|
||||
Password: created.Password,
|
||||
Username: &created.Username,
|
||||
Password: &created.Password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/typesafe"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
@@ -47,6 +49,7 @@ func NewOAuthHandler(oauthService *service.OAuthService) *OAuthHandler {
|
||||
|
||||
// AccountHandler handles admin account management
|
||||
type AccountHandler struct {
|
||||
claudeResetCredits claudeResetReader
|
||||
adminService service.AdminService
|
||||
oauthService *service.OAuthService
|
||||
openaiOAuthService *service.OpenAIOAuthService
|
||||
@@ -64,6 +67,8 @@ type AccountHandler struct {
|
||||
grokImportProber grokImportProber
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService
|
||||
cfg *config.Config
|
||||
opencodeGoUsage *service.OpenCodeGoUsageService
|
||||
}
|
||||
|
||||
// SetUpstreamBillingProbeService attaches the optional remote billing probe service.
|
||||
@@ -75,6 +80,10 @@ func (h *AccountHandler) SetOllamaCloudUsageService(usage *service.OllamaCloudUs
|
||||
h.ollamaCloudUsage = usage
|
||||
}
|
||||
|
||||
func (h *AccountHandler) SetOpenCodeGoUsageService(usage *service.OpenCodeGoUsageService) {
|
||||
h.opencodeGoUsage = usage
|
||||
}
|
||||
|
||||
// NewAccountHandler creates a new admin account handler
|
||||
func NewAccountHandler(
|
||||
adminService service.AdminService,
|
||||
@@ -190,6 +199,7 @@ type CheckMixedChannelRequest struct {
|
||||
// AccountWithConcurrency extends Account with real-time concurrency info
|
||||
type AccountWithConcurrency struct {
|
||||
*dto.Account
|
||||
simpleMode bool `json:"-"`
|
||||
CurrentConcurrency int `json:"current_concurrency"`
|
||||
SchedulerScore *AccountSchedulerScore `json:"scheduler_score,omitempty"`
|
||||
SchedulerScores []AccountSchedulerGroupScore `json:"scheduler_scores,omitempty"`
|
||||
@@ -199,6 +209,123 @@ type AccountWithConcurrency struct {
|
||||
CurrentRPM *int `json:"current_rpm,omitempty"` // 当前分钟 RPM 计数
|
||||
}
|
||||
|
||||
// AccountListItemWithConcurrency is the compact account-list envelope used
|
||||
// for lite=1. It embeds dto.AccountListItem instead of the full dto.Account,
|
||||
// so groups/account_groups never appear in the list payload.
|
||||
type AccountListItemWithConcurrency struct {
|
||||
*dto.AccountListItem
|
||||
CurrentConcurrency int `json:"current_concurrency"`
|
||||
SchedulerScore *AccountSchedulerScore `json:"scheduler_score,omitempty"`
|
||||
SchedulerScores []AccountSchedulerGroupScore `json:"scheduler_scores,omitempty"`
|
||||
CurrentWindowCost *float64 `json:"current_window_cost,omitempty"`
|
||||
ActiveSessions *int `json:"active_sessions,omitempty"`
|
||||
CurrentRPM *int `json:"current_rpm,omitempty"`
|
||||
}
|
||||
|
||||
type simpleModeGroupReference struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type simpleModeAccountGroupReference struct {
|
||||
AccountID int64 `json:"account_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
Priority int `json:"priority"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Group *simpleModeGroupReference `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
func simpleModeGroupReferenceFromDTO(group *dto.Group) *simpleModeGroupReference {
|
||||
if group == nil {
|
||||
return nil
|
||||
}
|
||||
return &simpleModeGroupReference{ID: group.ID, Name: group.Name, Platform: group.Platform, Status: group.Status}
|
||||
}
|
||||
|
||||
func simpleModeCompositeGroupIDs(account *dto.Account) map[int64]struct{} {
|
||||
hidden := make(map[int64]struct{})
|
||||
if account == nil {
|
||||
return hidden
|
||||
}
|
||||
for _, group := range account.Groups {
|
||||
if group != nil && group.Platform == service.PlatformComposite {
|
||||
hidden[group.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, accountGroup := range account.AccountGroups {
|
||||
if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
|
||||
hidden[accountGroup.GroupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return hidden
|
||||
}
|
||||
|
||||
func filterSimpleModeGroupIDs(groupIDs []int64, hidden map[int64]struct{}) []int64 {
|
||||
visible := make([]int64, 0, len(groupIDs))
|
||||
for _, groupID := range groupIDs {
|
||||
if _, ok := hidden[groupID]; !ok {
|
||||
visible = append(visible, groupID)
|
||||
}
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
func simpleModeCompositeServiceGroupIDs(account *service.Account) map[int64]struct{} {
|
||||
hidden := make(map[int64]struct{})
|
||||
if account == nil {
|
||||
return hidden
|
||||
}
|
||||
for _, group := range account.Groups {
|
||||
if group != nil && group.Platform == service.PlatformComposite {
|
||||
hidden[group.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, accountGroup := range account.AccountGroups {
|
||||
if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
|
||||
hidden[accountGroup.GroupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return hidden
|
||||
}
|
||||
|
||||
func (a AccountWithConcurrency) MarshalJSON() ([]byte, error) {
|
||||
type alias AccountWithConcurrency
|
||||
if !a.simpleMode || a.Account == nil {
|
||||
return json.Marshal(alias(a))
|
||||
}
|
||||
groups := make([]simpleModeGroupReference, 0, len(a.Groups))
|
||||
compositeIDs := simpleModeCompositeGroupIDs(a.Account)
|
||||
for _, group := range a.Groups {
|
||||
if group != nil && group.Platform == service.PlatformComposite {
|
||||
continue
|
||||
}
|
||||
if ref := simpleModeGroupReferenceFromDTO(group); ref != nil {
|
||||
groups = append(groups, *ref)
|
||||
}
|
||||
}
|
||||
accountGroups := make([]simpleModeAccountGroupReference, 0, len(a.AccountGroups))
|
||||
for _, accountGroup := range a.AccountGroups {
|
||||
if accountGroup.Group != nil && accountGroup.Group.Platform == service.PlatformComposite {
|
||||
continue
|
||||
}
|
||||
if _, hidden := compositeIDs[accountGroup.GroupID]; hidden {
|
||||
continue
|
||||
}
|
||||
accountGroups = append(accountGroups, simpleModeAccountGroupReference{
|
||||
AccountID: accountGroup.AccountID, GroupID: accountGroup.GroupID, Priority: accountGroup.Priority,
|
||||
CreatedAt: accountGroup.CreatedAt, Group: simpleModeGroupReferenceFromDTO(accountGroup.Group),
|
||||
})
|
||||
}
|
||||
return json.Marshal(struct {
|
||||
alias
|
||||
GroupIDs []int64 `json:"group_ids,omitempty"`
|
||||
Groups []simpleModeGroupReference `json:"groups"`
|
||||
AccountGroups []simpleModeAccountGroupReference `json:"account_groups"`
|
||||
}{alias: alias(a), GroupIDs: filterSimpleModeGroupIDs(a.GroupIDs, compositeIDs), Groups: groups, AccountGroups: accountGroups})
|
||||
}
|
||||
|
||||
type AccountSchedulerScore struct {
|
||||
BaseScore float64 `json:"base_score"`
|
||||
StickyScore float64 `json:"sticky_score"`
|
||||
@@ -223,9 +350,25 @@ func (h *AccountHandler) accountResponseFromService(account *service.Account) *d
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *AccountHandler) accountListResponseFromService(account *service.Account) *dto.Account {
|
||||
out := dto.AccountFromServiceShallow(account)
|
||||
if out != nil && account != nil {
|
||||
out.Proxy = dto.ProxyFromService(account.Proxy)
|
||||
}
|
||||
if h != nil && h.ollamaCloudUsage != nil && out != nil {
|
||||
h.ollamaCloudUsage.EnrichState(out.OllamaCloudUsage)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *AccountHandler) isSimpleMode() bool {
|
||||
return h != nil && h.cfg != nil && h.cfg.RunMode == config.RunModeSimple
|
||||
}
|
||||
|
||||
func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, account *service.Account) AccountWithConcurrency {
|
||||
item := AccountWithConcurrency{
|
||||
Account: h.accountResponseFromService(account),
|
||||
simpleMode: h.isSimpleMode(),
|
||||
CurrentConcurrency: 0,
|
||||
}
|
||||
if account == nil {
|
||||
@@ -538,14 +681,22 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if h.ollamaCloudUsage != nil && len(accounts) > 0 {
|
||||
if len(accounts) > 0 {
|
||||
accountPointers := make([]*service.Account, len(accounts))
|
||||
for index := range accounts {
|
||||
accountPointers[index] = &accounts[index]
|
||||
}
|
||||
if err := h.ollamaCloudUsage.ResolveAccounts(c.Request.Context(), accountPointers); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
if h.ollamaCloudUsage != nil {
|
||||
if err := h.ollamaCloudUsage.ResolveAccounts(c.Request.Context(), accountPointers); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if h.opencodeGoUsage != nil {
|
||||
if err := h.opencodeGoUsage.ResolveOpenCodeGoUsageAccounts(c.Request.Context(), accountPointers); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,8 +801,16 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
result := make([]AccountWithConcurrency, len(accounts))
|
||||
for i := range accounts {
|
||||
acc := &accounts[i]
|
||||
accountResponse := h.accountResponseFromService(acc)
|
||||
if lite {
|
||||
accountResponse = h.accountListResponseFromService(acc)
|
||||
if h.isSimpleMode() {
|
||||
accountResponse.GroupIDs = filterSimpleModeGroupIDs(accountResponse.GroupIDs, simpleModeCompositeServiceGroupIDs(acc))
|
||||
}
|
||||
}
|
||||
item := AccountWithConcurrency{
|
||||
Account: h.accountResponseFromService(acc),
|
||||
Account: accountResponse,
|
||||
simpleMode: h.isSimpleMode(),
|
||||
CurrentConcurrency: concurrencyCounts[acc.ID],
|
||||
SchedulerScore: schedulerScores[acc.ID],
|
||||
SchedulerScores: schedulerGroupScores[acc.ID],
|
||||
@@ -683,7 +842,34 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
|
||||
h.enrichShadowParents(c.Request.Context(), result)
|
||||
|
||||
etag := buildAccountsListETag(result, total, page, pageSize, platform, accountType, status, search, lite)
|
||||
if lite {
|
||||
compact := make([]AccountListItemWithConcurrency, len(result))
|
||||
for i := range result {
|
||||
item := result[i]
|
||||
compact[i] = AccountListItemWithConcurrency{
|
||||
AccountListItem: dto.AccountListItemFromAccount(item.Account),
|
||||
CurrentConcurrency: item.CurrentConcurrency,
|
||||
SchedulerScore: item.SchedulerScore,
|
||||
SchedulerScores: item.SchedulerScores,
|
||||
CurrentWindowCost: item.CurrentWindowCost,
|
||||
ActiveSessions: item.ActiveSessions,
|
||||
CurrentRPM: item.CurrentRPM,
|
||||
}
|
||||
}
|
||||
etag := buildAccountsListETag(compact, total, page, pageSize, platform, accountType, status, search, true)
|
||||
if etag != "" {
|
||||
c.Header("ETag", etag)
|
||||
c.Header("Vary", "If-None-Match")
|
||||
if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
response.Paginated(c, compact, total, page, pageSize)
|
||||
return
|
||||
}
|
||||
|
||||
etag := buildAccountsListETag(result, total, page, pageSize, platform, accountType, status, search, false)
|
||||
if etag != "" {
|
||||
c.Header("ETag", etag)
|
||||
c.Header("Vary", "If-None-Match")
|
||||
@@ -696,23 +882,23 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
response.Paginated(c, result, total, page, pageSize)
|
||||
}
|
||||
|
||||
func buildAccountsListETag(
|
||||
items []AccountWithConcurrency,
|
||||
func buildAccountsListETag[T any](
|
||||
items []T,
|
||||
total int64,
|
||||
page, pageSize int,
|
||||
platform, accountType, status, search string,
|
||||
lite bool,
|
||||
) string {
|
||||
payload := struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Platform string `json:"platform"`
|
||||
AccountType string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Search string `json:"search"`
|
||||
Lite bool `json:"lite"`
|
||||
Items []AccountWithConcurrency `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Platform string `json:"platform"`
|
||||
AccountType string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Search string `json:"search"`
|
||||
Lite bool `json:"lite"`
|
||||
Items []T `json:"items"`
|
||||
}{
|
||||
Total: total,
|
||||
Page: page,
|
||||
@@ -771,6 +957,12 @@ func (h *AccountHandler) GetByID(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if h.opencodeGoUsage != nil {
|
||||
if err := h.opencodeGoUsage.ResolveOpenCodeGoUsageAccounts(c.Request.Context(), []*service.Account{account}); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||
}
|
||||
@@ -837,6 +1029,10 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
||||
}
|
||||
// base_rpm 输入校验:负值归零,超过 10000 截断
|
||||
sanitizeExtraBaseRPM(req.Extra)
|
||||
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 确定是否跳过混合渠道检查
|
||||
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
|
||||
@@ -970,6 +1166,10 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
||||
}
|
||||
// base_rpm 输入校验:负值归零,超过 10000 截断
|
||||
sanitizeExtraBaseRPM(req.Extra)
|
||||
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 确定是否跳过混合渠道检查
|
||||
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
|
||||
@@ -1366,6 +1566,7 @@ func (h *AccountHandler) Refresh(c *gin.Context) {
|
||||
|
||||
if warning == "missing_project_id_temporary" {
|
||||
response.Success(c, gin.H{
|
||||
"account": h.buildAccountResponseWithRuntime(c.Request.Context(), updatedAccount),
|
||||
"message": "Token refreshed successfully, but project_id could not be retrieved (will retry automatically)",
|
||||
"warning": "missing_project_id_temporary",
|
||||
})
|
||||
@@ -1424,7 +1625,15 @@ func (h *AccountHandler) ApplyOAuthCredentials(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-auth only returns authentication fields. Preserve account configuration
|
||||
// stored alongside them (for example model_mapping), while allowing the new
|
||||
// OAuth values to replace their existing counterparts.
|
||||
req.Credentials = service.MergeCredentials(existing.Credentials, req.Credentials)
|
||||
// Drop SSO/password residue; re-auth must leave only OAuth tokens on disk.
|
||||
req.Credentials = service.SanitizeStoredCredentials(existing.Platform, req.Credentials)
|
||||
|
||||
@@ -1875,6 +2084,14 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
groupIDs := make([]int64, 0)
|
||||
for _, item := range req.Accounts {
|
||||
groupIDs = append(groupIDs, item.GroupIDs...)
|
||||
}
|
||||
if err := h.adminService.ValidateAccountGroupBindings(c.Request.Context(), groupIDs); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
executeAdminIdempotentJSON(c, "admin.accounts.batch_create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||
success := 0
|
||||
@@ -1897,6 +2114,15 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
|
||||
|
||||
// base_rpm 输入校验:负值归零,超过 10000 截断
|
||||
sanitizeExtraBaseRPM(item.Extra)
|
||||
if err := service.ValidateUpstreamRequestIDHeaderExtra(item.Extra); err != nil {
|
||||
failed++
|
||||
results = append(results, gin.H{
|
||||
"name": item.Name,
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
skipCheck := item.ConfirmMixedChannelRisk != nil && *item.ConfirmMixedChannelRisk
|
||||
|
||||
@@ -2091,6 +2317,10 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
||||
}
|
||||
// base_rpm 输入校验:负值归零,超过 10000 截断
|
||||
sanitizeExtraBaseRPM(req.Extra)
|
||||
if err := service.ValidateUpstreamRequestIDHeaderExtra(req.Extra); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 确定是否跳过混合渠道检查
|
||||
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
|
||||
@@ -2576,6 +2806,14 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
|
||||
|
||||
// Handle OpenAI accounts
|
||||
if account.IsOpenAI() {
|
||||
// Prefer the shared, account-keyed upstream catalog. If discovery fails,
|
||||
// retain the legacy local catalog below so the test dialog remains usable.
|
||||
if h.accountTestService != nil {
|
||||
if models, fetchErr := h.accountTestService.FetchOpenAIAccountModels(c.Request.Context(), account); fetchErr == nil {
|
||||
response.Success(c, models)
|
||||
return
|
||||
}
|
||||
}
|
||||
// OpenAI 自动透传会绕过常规模型改写,测试/模型列表也应回落到默认模型集。
|
||||
if account.IsOpenAIPassthroughEnabled() {
|
||||
response.Success(c, openai.DefaultModels)
|
||||
@@ -2713,6 +2951,12 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TypeSafe accounts serve only the native System One model.
|
||||
if account.IsTypeSafe() {
|
||||
response.Success(c, []claude.Model{{ID: typesafe.JevLatestModel, Type: "model", DisplayName: typesafe.JevLatestModel}})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Claude/Anthropic accounts
|
||||
// For OAuth and Setup-Token accounts: return default models
|
||||
if account.IsOAuth() {
|
||||
@@ -2774,13 +3018,15 @@ func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
models, err := h.accountTestService.FetchUpstreamSupportedModels(c.Request.Context(), account)
|
||||
catalog, err := h.accountTestService.SyncUpstreamModelCatalog(c.Request.Context(), account)
|
||||
if err != nil {
|
||||
var syncErr *service.UpstreamModelSyncError
|
||||
if errors.As(err, &syncErr) {
|
||||
switch syncErr.Kind {
|
||||
case service.UpstreamModelSyncErrorConfiguration, service.UpstreamModelSyncErrorUnsupported:
|
||||
response.BadRequest(c, syncErr.SafeMessage())
|
||||
case service.UpstreamModelSyncErrorInternal:
|
||||
response.InternalError(c, syncErr.SafeMessage())
|
||||
default:
|
||||
slog.Warn("sync_upstream_models_failed", "account_id", accountID, "kind", syncErr.Kind)
|
||||
response.Error(c, http.StatusBadGateway, syncErr.SafeMessage())
|
||||
@@ -2793,29 +3039,35 @@ func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"models": models})
|
||||
response.Success(c, catalog)
|
||||
}
|
||||
|
||||
// SyncUpstreamModelsPreview handles syncing live supported models using provided credentials (no account ID needed).
|
||||
// POST /api/v1/admin/accounts/models/sync-upstream-preview
|
||||
func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
|
||||
var req struct {
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key" binding:"required"`
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key" binding:"required"`
|
||||
ModelMapping map[string]string `json:"model_mapping"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
modelMapping := make(map[string]any, len(req.ModelMapping))
|
||||
for sourceModel, upstreamModel := range req.ModelMapping {
|
||||
modelMapping[sourceModel] = upstreamModel
|
||||
}
|
||||
|
||||
tempAccount := &service.Account{
|
||||
Platform: req.Platform,
|
||||
Type: req.Type,
|
||||
Credentials: map[string]any{
|
||||
"api_key": req.APIKey,
|
||||
"base_url": req.BaseURL,
|
||||
"api_key": req.APIKey,
|
||||
"base_url": req.BaseURL,
|
||||
"model_mapping": modelMapping,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2824,13 +3076,15 @@ func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
models, err := h.accountTestService.FetchUpstreamSupportedModels(c.Request.Context(), tempAccount)
|
||||
catalog, err := h.accountTestService.SyncUpstreamModelCatalog(c.Request.Context(), tempAccount)
|
||||
if err != nil {
|
||||
var syncErr *service.UpstreamModelSyncError
|
||||
if errors.As(err, &syncErr) {
|
||||
switch syncErr.Kind {
|
||||
case service.UpstreamModelSyncErrorConfiguration, service.UpstreamModelSyncErrorUnsupported:
|
||||
response.BadRequest(c, syncErr.SafeMessage())
|
||||
case service.UpstreamModelSyncErrorInternal:
|
||||
response.InternalError(c, syncErr.SafeMessage())
|
||||
default:
|
||||
slog.Warn("sync_upstream_models_preview_failed", "platform", req.Platform, "kind", syncErr.Kind)
|
||||
response.Error(c, http.StatusBadGateway, syncErr.SafeMessage())
|
||||
@@ -2843,7 +3097,7 @@ func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"models": models})
|
||||
response.Success(c, catalog)
|
||||
}
|
||||
|
||||
// SetPrivacy handles setting privacy for a single OpenAI/Antigravity OAuth account
|
||||
|
||||
@@ -38,14 +38,20 @@ func setupAvailableModelsRouter(adminSvc service.AdminService) *gin.Engine {
|
||||
}
|
||||
|
||||
type syncUpstreamHTTPUpstream struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
resp *http.Response
|
||||
responses []*http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
func (u *syncUpstreamHTTPUpstream) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
|
||||
if u.err != nil {
|
||||
return nil, u.err
|
||||
}
|
||||
if len(u.responses) > 0 {
|
||||
resp := u.responses[0]
|
||||
u.responses = u.responses[1:]
|
||||
return resp, nil
|
||||
}
|
||||
return u.resp, nil
|
||||
}
|
||||
|
||||
@@ -68,6 +74,7 @@ func setupSyncUpstreamModelsRouter(adminSvc service.AdminService, upstream servi
|
||||
)
|
||||
handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, accountTestSvc, nil, nil, nil, nil, nil)
|
||||
router.POST("/api/v1/admin/accounts/:id/models/sync-upstream", handler.SyncUpstreamModels)
|
||||
router.POST("/api/v1/admin/accounts/models/sync-upstream-preview", handler.SyncUpstreamModelsPreview)
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -347,6 +354,99 @@ func TestAccountHandlerSyncUpstreamModels_ConfigErrorReturnsBadRequest(t *testin
|
||||
require.Contains(t, rec.Body.String(), "No OpenAI API key is available")
|
||||
}
|
||||
|
||||
func TestAccountHandlerSyncUpstreamModelsReturnsCapabilityMetadata(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
account: service.Account{
|
||||
ID: 48, Name: "custom-openai", Platform: service.PlatformOpenAI,
|
||||
Type: service.AccountTypeAPIKey, Status: service.StatusActive,
|
||||
Credentials: map[string]any{"api_key": "key", "base_url": "https://provider.example/v1"},
|
||||
},
|
||||
}
|
||||
upstream := &syncUpstreamHTTPUpstream{resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"models":[{
|
||||
"id":"custom-thinking-model",
|
||||
"reasoning":true,
|
||||
"default_reasoning_level":"high",
|
||||
"supported_reasoning_levels":["low","high"],
|
||||
"input_modalities":["text","image"],
|
||||
"context_window":256000
|
||||
}]}`)),
|
||||
}}
|
||||
router := setupSyncUpstreamModelsRouter(svc, upstream)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/48/models/sync-upstream", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var resp struct {
|
||||
Data service.UpstreamModelCatalog `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
require.Equal(t, []string{"custom-thinking-model"}, resp.Data.Models)
|
||||
metadata := resp.Data.Metadata["custom-thinking-model"]
|
||||
require.NotNil(t, metadata.Reasoning)
|
||||
require.True(t, *metadata.Reasoning)
|
||||
require.Equal(t, []string{"low", "high"}, metadata.SupportedReasoningLevels)
|
||||
require.Equal(t, []string{"text", "image"}, metadata.InputModalities)
|
||||
}
|
||||
|
||||
// Scenario: 创建账号 preview 将具体 mapping 传给 404/405 配置回退。
|
||||
func TestAccountHandlerSyncUpstreamModelsPreviewUsesProvidedModelMapping(t *testing.T) {
|
||||
upstream := &syncUpstreamHTTPUpstream{responses: []*http.Response{
|
||||
{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"error":"not found"}`)),
|
||||
},
|
||||
{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{
|
||||
"configured-provider": {
|
||||
"api": "https://provider.example/v1",
|
||||
"models": {
|
||||
"glm-5.3": {
|
||||
"id": "glm-5.3",
|
||||
"reasoning": true,
|
||||
"reasoning_options": [{"type":"effort","values":["low","high"]}],
|
||||
"modalities": {"input":["text"],"output":["text"]},
|
||||
"limit": {"context":1000000,"output":131072}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
},
|
||||
}}
|
||||
router := setupSyncUpstreamModelsRouter(newStubAdminService(), upstream)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/v1/admin/accounts/models/sync-upstream-preview",
|
||||
strings.NewReader(`{
|
||||
"platform":"openai",
|
||||
"type":"apikey",
|
||||
"base_url":"https://provider.example/v1",
|
||||
"api_key":"key",
|
||||
"model_mapping":{"public-glm":"glm-5.3"}
|
||||
}`),
|
||||
)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var resp struct {
|
||||
Data service.UpstreamModelCatalog `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
require.Equal(t, []string{"glm-5.3"}, resp.Data.Models)
|
||||
require.Equal(t, []string{"low", "high"}, resp.Data.Metadata["glm-5.3"].SupportedReasoningLevels)
|
||||
}
|
||||
|
||||
func TestAccountHandlerSyncUpstreamModels_UpstreamErrorDoesNotExposeBody(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
@@ -377,3 +477,85 @@ func TestAccountHandlerSyncUpstreamModels_UpstreamErrorDoesNotExposeBody(t *test
|
||||
require.Contains(t, rec.Body.String(), "Upstream model list request failed with HTTP 502")
|
||||
require.NotContains(t, rec.Body.String(), "SECRET_TOKEN")
|
||||
}
|
||||
|
||||
// Scenario: 能力补全失败显示部分成功。
|
||||
func TestAccountHandlerSyncUpstreamModels_MetadataEnrichmentFailureReturnsWarning(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
account: service.Account{
|
||||
ID: 46,
|
||||
Name: "opencode-id-only-model-list",
|
||||
Platform: service.PlatformOpenAI,
|
||||
Type: service.AccountTypeAPIKey,
|
||||
Status: service.StatusActive,
|
||||
Credentials: map[string]any{
|
||||
"api_key": "opencode-key",
|
||||
"base_url": "https://opencode.ai/zen/v1",
|
||||
},
|
||||
},
|
||||
}
|
||||
upstream := &syncUpstreamHTTPUpstream{responses: []*http.Response{
|
||||
{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"x-preview-f-free"}]}`)),
|
||||
},
|
||||
{
|
||||
StatusCode: http.StatusBadGateway,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"error":"registry unavailable"}`)),
|
||||
},
|
||||
}}
|
||||
router := setupSyncUpstreamModelsRouter(svc, upstream)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/46/models/sync-upstream", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var resp struct {
|
||||
Data struct {
|
||||
Models []string `json:"models"`
|
||||
Warnings []struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"warnings"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
require.Equal(t, []string{"x-preview-f-free"}, resp.Data.Models)
|
||||
require.Len(t, resp.Data.Warnings, 1)
|
||||
require.Equal(t, "upstream_model_metadata_incomplete", resp.Data.Warnings[0].Code)
|
||||
}
|
||||
|
||||
func TestAccountHandlerGetAvailableModels_TypeSafeOnlyReturnsJev(t *testing.T) {
|
||||
for _, credentials := range []map[string]any{
|
||||
{"api_key": "ts-secret"},
|
||||
{"api_key": "ts-secret", "model_mapping": map[string]any{"jev-latest": "jev-latest"}},
|
||||
} {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
account: service.Account{
|
||||
ID: 46,
|
||||
Name: "typesafe",
|
||||
Platform: service.PlatformTypeSafe,
|
||||
Type: service.AccountTypeAPIKey,
|
||||
Status: service.StatusActive,
|
||||
Credentials: credentials,
|
||||
},
|
||||
}
|
||||
router := setupAvailableModelsRouter(svc)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/46/models", nil))
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var resp struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
require.Len(t, resp.Data, 1)
|
||||
require.Equal(t, "jev-latest", resp.Data[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,6 +16,101 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAccountHandlerListLiteUsesCompactDTOAndETag(t *testing.T) {
|
||||
router, adminSvc := setupAccountListRouter()
|
||||
now := time.Now().UTC()
|
||||
groupID := int64(77)
|
||||
adminSvc.accounts = []service.Account{{
|
||||
ID: 501, Name: "compact-account", Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth,
|
||||
Credentials: map[string]any{"email": "compact@example.com", "access_token": strings.Repeat("x", 4096)},
|
||||
Extra: map[string]any{"privacy_mode": "training_off"}, Status: service.StatusActive,
|
||||
Schedulable: true, Concurrency: 4, GroupIDs: []int64{groupID},
|
||||
Groups: []*service.Group{{ID: groupID, Name: "codex", Platform: service.PlatformOpenAI}},
|
||||
AccountGroups: []service.AccountGroup{{AccountID: 501, GroupID: groupID, Priority: 2, Group: &service.Group{ID: groupID, Name: "codex", Platform: service.PlatformOpenAI}}},
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
}}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.NotEmpty(t, rec.Header().Get("ETag"))
|
||||
|
||||
var litePayload struct {
|
||||
Data struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &litePayload))
|
||||
require.Len(t, litePayload.Data.Items, 1)
|
||||
liteItem := litePayload.Data.Items[0]
|
||||
require.Equal(t, float64(501), liteItem["id"])
|
||||
require.Equal(t, []any{float64(groupID)}, liteItem["group_ids"])
|
||||
require.Equal(t, true, liteItem["schedulable"])
|
||||
require.NotContains(t, liteItem, "groups")
|
||||
require.NotContains(t, liteItem, "account_groups")
|
||||
credentials, ok := liteItem["credentials"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "compact@example.com", credentials["email"])
|
||||
require.NotContains(t, credentials, "access_token")
|
||||
credentialsStatus, ok := liteItem["credentials_status"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, true, credentialsStatus["has_access_token"])
|
||||
|
||||
// The ETag must represent the same compact body and return 304 on refresh.
|
||||
rec304 := httptest.NewRecorder()
|
||||
req304 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
|
||||
req304.Header.Set("If-None-Match", rec.Header().Get("ETag"))
|
||||
router.ServeHTTP(rec304, req304)
|
||||
require.Equal(t, http.StatusNotModified, rec304.Code)
|
||||
|
||||
// Omitting lite preserves the legacy full response shape.
|
||||
recFull := httptest.NewRecorder()
|
||||
reqFull := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20", nil)
|
||||
router.ServeHTTP(recFull, reqFull)
|
||||
require.Equal(t, http.StatusOK, recFull.Code)
|
||||
var fullPayload struct {
|
||||
Data struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(recFull.Body.Bytes(), &fullPayload))
|
||||
require.Contains(t, fullPayload.Data.Items[0], "groups")
|
||||
require.Contains(t, fullPayload.Data.Items[0], "account_groups")
|
||||
}
|
||||
|
||||
func TestAccountHandlerListLiteStaysBelowResponseBudget(t *testing.T) {
|
||||
router, adminSvc := setupAccountListRouter()
|
||||
now := time.Now().UTC()
|
||||
accounts := make([]service.Account, 20)
|
||||
for i := range accounts {
|
||||
id := int64(600 + i)
|
||||
groupID := int64(800 + i)
|
||||
accounts[i] = service.Account{
|
||||
ID: id, Name: "account-" + strconv.Itoa(i), Platform: service.PlatformOpenAI,
|
||||
Type: service.AccountTypeOAuth, Status: service.StatusActive, Schedulable: true,
|
||||
Concurrency: 4, GroupIDs: []int64{groupID},
|
||||
Groups: []*service.Group{{ID: groupID, Name: "group-" + strconv.Itoa(i), Description: strings.Repeat("description ", 20)}},
|
||||
AccountGroups: []service.AccountGroup{{AccountID: id, GroupID: groupID, Group: &service.Group{ID: groupID, Name: "group-" + strconv.Itoa(i), Description: strings.Repeat("description ", 20)}}},
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
adminSvc.accounts = accounts
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&lite=1", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Less(t, rec.Body.Len(), 80*1024)
|
||||
|
||||
var compressed bytes.Buffer
|
||||
zw := gzip.NewWriter(&compressed)
|
||||
_, err := zw.Write(rec.Body.Bytes())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, zw.Close())
|
||||
require.Less(t, compressed.Len(), 15*1024)
|
||||
}
|
||||
|
||||
func setupAccountListRouter() (*gin.Engine, *stubAdminService) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
|
||||
@@ -142,6 +142,44 @@ func TestApplyOAuthCredentialsRejectsMalformedOpenAILongContextBillingBeforeMuta
|
||||
require.Zero(t, stub.updateAccountExtraCalls)
|
||||
}
|
||||
|
||||
func TestApplyOAuthCredentialsPreservesExistingNonAuthCredentials(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
stub := newStubAdminService()
|
||||
stub.getAccountResult = &service.Account{
|
||||
ID: 1,
|
||||
Platform: service.PlatformOpenAI,
|
||||
Type: service.AccountTypeOAuth,
|
||||
Credentials: map[string]any{
|
||||
"access_token": "old-token",
|
||||
"refresh_token": "old-refresh-token",
|
||||
"model_mapping": map[string]any{"gpt-5": "gpt-5"},
|
||||
"account_id": "existing-account-id",
|
||||
"password": "must-not-survive",
|
||||
"sso_token": "must-not-survive",
|
||||
"cookie": "must-not-survive",
|
||||
},
|
||||
}
|
||||
handler := NewAccountHandler(stub, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
router := gin.New()
|
||||
router.POST("/accounts/:id/apply-oauth-credentials", handler.ApplyOAuthCredentials)
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodPost, "/accounts/1/apply-oauth-credentials", bytes.NewBufferString(
|
||||
`{"type":"oauth","credentials":{"access_token":"new-token","refresh_token":"new-refresh-token"}}`,
|
||||
))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Equal(t, 1, stub.updateAccountCalls)
|
||||
require.Equal(t, map[string]any{
|
||||
"access_token": "new-token",
|
||||
"refresh_token": "new-refresh-token",
|
||||
"model_mapping": map[string]any{"gpt-5": "gpt-5"},
|
||||
"account_id": "existing-account-id",
|
||||
}, stub.lastUpdateAccountInput.Credentials)
|
||||
}
|
||||
|
||||
func TestOpenAIOAuthCodexPATBoundaryRejectsMalformedOpenAILongContextBillingValueBeforeTokenValidation(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
handler := NewOpenAIOAuthHandler(nil, newStubAdminService(), nil, nil)
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type simpleModeAccountService struct {
|
||||
*stubAdminService
|
||||
account service.Account
|
||||
createCalls int
|
||||
updateCalls int
|
||||
bulkCalls int
|
||||
}
|
||||
|
||||
func (s *simpleModeAccountService) GetAccount(context.Context, int64) (*service.Account, error) {
|
||||
return &s.account, nil
|
||||
}
|
||||
|
||||
func (s *simpleModeAccountService) CreateAccount(ctx context.Context, input *service.CreateAccountInput) (*service.Account, error) {
|
||||
if err := s.ValidateAccountGroupBindings(ctx, input.GroupIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.createCalls++
|
||||
return &s.account, nil
|
||||
}
|
||||
|
||||
func (s *simpleModeAccountService) UpdateAccount(ctx context.Context, _ int64, input *service.UpdateAccountInput) (*service.Account, error) {
|
||||
if input.GroupIDs != nil {
|
||||
if err := s.ValidateAccountGroupBindings(ctx, *input.GroupIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s.updateCalls++
|
||||
return &s.account, nil
|
||||
}
|
||||
|
||||
func (s *simpleModeAccountService) BulkUpdateAccounts(ctx context.Context, input *service.BulkUpdateAccountsInput) (*service.BulkUpdateAccountsResult, error) {
|
||||
if input.GroupIDs != nil {
|
||||
if err := s.ValidateAccountGroupBindings(ctx, *input.GroupIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s.bulkCalls++
|
||||
return &service.BulkUpdateAccountsResult{}, nil
|
||||
}
|
||||
|
||||
func (s *simpleModeAccountService) ValidateAccountGroupBindings(_ context.Context, groupIDs []int64) error {
|
||||
for _, id := range groupIDs {
|
||||
for i := range s.groups {
|
||||
if s.groups[i].ID == id && s.groups[i].Platform == service.PlatformComposite {
|
||||
return infraerrors.BadRequest("SIMPLE_MODE_GROUP_NOT_BINDABLE", "composite groups cannot be bound in simple mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAccountHandlerSimpleModeUsesMinimalGroupReferences(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
richGroup := &service.Group{ID: 7, Name: "basic", Platform: service.PlatformAnthropic, Status: service.StatusActive, RateMultiplier: 9, IsExclusive: true, SubscriptionType: service.SubscriptionTypeSubscription, RPMLimit: 99}
|
||||
historicalComposite := &service.Group{ID: 9, Name: "historical composite", Platform: service.PlatformComposite, Status: service.StatusActive}
|
||||
account := service.Account{
|
||||
ID: 3, Name: "account", Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey, Status: service.StatusActive,
|
||||
GroupIDs: []int64{7, 9},
|
||||
Groups: []*service.Group{richGroup, historicalComposite},
|
||||
AccountGroups: []service.AccountGroup{
|
||||
{AccountID: 3, GroupID: 7, Priority: 2, Group: richGroup, Account: &service.Account{ID: 3, Extra: map[string]any{"secret": true}}},
|
||||
{AccountID: 3, GroupID: 9, Priority: 3, Group: historicalComposite},
|
||||
},
|
||||
}
|
||||
svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: account}
|
||||
svc.accounts = []service.Account{account}
|
||||
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
h.cfg = &config.Config{RunMode: config.RunModeSimple}
|
||||
r := gin.New()
|
||||
r.GET("/accounts", h.List)
|
||||
r.GET("/accounts/:id", h.GetByID)
|
||||
r.POST("/accounts", h.Create)
|
||||
r.PUT("/accounts/:id", h.Update)
|
||||
|
||||
tests := []struct {
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
}{
|
||||
{http.MethodGet, "/accounts", ""},
|
||||
{http.MethodGet, "/accounts/3", ""},
|
||||
{http.MethodPost, "/accounts", `{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"}}`},
|
||||
{http.MethodPut, "/accounts/3", `{"name":"account"}`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.method+tt.path, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
r.ServeHTTP(res, req)
|
||||
require.Equal(t, http.StatusOK, res.Code, res.Body.String())
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(res.Body.Bytes(), &payload))
|
||||
data, ok := payload["data"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
if items, ok := data["items"].([]any); ok {
|
||||
require.Len(t, items, 1)
|
||||
data, ok = items[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
}
|
||||
groups, ok := data["groups"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, groups, 1)
|
||||
require.Equal(t, []any{float64(7)}, data["group_ids"])
|
||||
group, ok := groups[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.ElementsMatch(t, []string{"id", "name", "platform", "status"}, mapKeys(group))
|
||||
accountGroups, ok := data["account_groups"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, accountGroups, 1)
|
||||
accountGroup, ok := accountGroups[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.ElementsMatch(t, []string{"account_id", "group_id", "priority", "created_at", "group"}, mapKeys(accountGroup))
|
||||
nestedGroup, ok := accountGroup["group"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.ElementsMatch(t, []string{"id", "name", "platform", "status"}, mapKeys(nestedGroup))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountHandlerSimpleModeLitePreservesCompactShapeAndETag(t *testing.T) {
|
||||
richGroup := &service.Group{ID: 7, Name: "basic", Platform: service.PlatformAnthropic, Status: service.StatusActive, RateMultiplier: 9}
|
||||
historicalComposite := &service.Group{ID: 9, Name: "historical composite", Platform: service.PlatformComposite, Status: service.StatusActive}
|
||||
account := service.Account{
|
||||
ID: 3, Name: "account", Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey, Status: service.StatusActive,
|
||||
GroupIDs: []int64{7, 9}, Groups: []*service.Group{richGroup, historicalComposite},
|
||||
AccountGroups: []service.AccountGroup{{AccountID: 3, GroupID: 7, Group: richGroup}, {AccountID: 3, GroupID: 9, Group: historicalComposite}},
|
||||
}
|
||||
svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: account}
|
||||
svc.accounts = []service.Account{account}
|
||||
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
h.cfg = &config.Config{RunMode: config.RunModeSimple}
|
||||
r := gin.New()
|
||||
r.GET("/accounts", h.List)
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
r.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/accounts?lite=1", nil))
|
||||
require.Equal(t, http.StatusOK, res.Code, res.Body.String())
|
||||
require.NotEmpty(t, res.Header().Get("ETag"))
|
||||
var payload struct {
|
||||
Data struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(res.Body.Bytes(), &payload))
|
||||
require.Len(t, payload.Data.Items, 1)
|
||||
require.Equal(t, []any{float64(7)}, payload.Data.Items[0]["group_ids"])
|
||||
require.NotContains(t, payload.Data.Items[0], "groups")
|
||||
require.NotContains(t, payload.Data.Items[0], "account_groups")
|
||||
|
||||
notModified := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/accounts?lite=1", nil)
|
||||
req.Header.Set("If-None-Match", res.Header().Get("ETag"))
|
||||
r.ServeHTTP(notModified, req)
|
||||
require.Equal(t, http.StatusNotModified, notModified.Code)
|
||||
}
|
||||
|
||||
func TestAccountHandlerSimpleModeRejectsCompositeGroupBindingsBeforeWrites(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, tt := range []struct{ name, method, path, body string }{
|
||||
{"create", http.MethodPost, "/accounts", `{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[9]}`},
|
||||
{"update", http.MethodPut, "/accounts/3", `{"group_ids":[9]}`},
|
||||
{"bulk", http.MethodPost, "/accounts/bulk-update", `{"account_ids":[3],"group_ids":[9]}`},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
svc := &simpleModeAccountService{stubAdminService: newStubAdminService()}
|
||||
svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}, {ID: 9, Platform: service.PlatformComposite}}
|
||||
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
h.cfg = &config.Config{RunMode: config.RunModeSimple}
|
||||
r := gin.New()
|
||||
r.POST("/accounts", h.Create)
|
||||
r.PUT("/accounts/:id", h.Update)
|
||||
r.POST("/accounts/bulk-update", h.BulkUpdate)
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(res, req)
|
||||
require.Equal(t, http.StatusBadRequest, res.Code, res.Body.String())
|
||||
require.Zero(t, svc.createCalls)
|
||||
require.Zero(t, svc.updateCalls)
|
||||
require.Zero(t, svc.bulkCalls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountHandlerSimpleModePreservesBasicGroupBinding(t *testing.T) {
|
||||
svc := &simpleModeAccountService{stubAdminService: newStubAdminService(), account: service.Account{ID: 3}}
|
||||
svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}}
|
||||
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
h.cfg = &config.Config{RunMode: config.RunModeSimple}
|
||||
r := gin.New()
|
||||
r.POST("/accounts", h.Create)
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/accounts", bytes.NewBufferString(`{"name":"account","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[7]}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(res, req)
|
||||
require.Equal(t, http.StatusOK, res.Code, res.Body.String())
|
||||
require.Equal(t, 1, svc.createCalls)
|
||||
}
|
||||
|
||||
func TestAccountHandlerSimpleModeBatchPrevalidatesAllGroupsAtomically(t *testing.T) {
|
||||
svc := &simpleModeAccountService{stubAdminService: newStubAdminService()}
|
||||
svc.groups = []service.Group{{ID: 7, Platform: service.PlatformAnthropic}, {ID: 9, Platform: service.PlatformComposite}}
|
||||
h := NewAccountHandler(svc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
h.cfg = &config.Config{RunMode: config.RunModeSimple}
|
||||
r := gin.New()
|
||||
r.POST("/accounts/batch", h.BatchCreate)
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/accounts/batch", bytes.NewBufferString(`{"accounts":[{"name":"first","platform":"anthropic","type":"apikey","credentials":{"key":"x"},"group_ids":[7]},{"name":"second","platform":"anthropic","type":"apikey","credentials":{"key":"y"},"group_ids":[9]}]}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(res, req)
|
||||
require.Equal(t, http.StatusBadRequest, res.Code, res.Body.String())
|
||||
require.Zero(t, svc.createCalls)
|
||||
}
|
||||
|
||||
func TestAccountHandlerAdvancedModeKeepsFullGroupReferences(t *testing.T) {
|
||||
group := &service.Group{ID: 7, Name: "advanced", RateMultiplier: 9, SubscriptionType: service.SubscriptionTypeSubscription}
|
||||
h := NewAccountHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
raw, err := json.Marshal(h.buildAccountResponseWithRuntime(context.Background(), &service.Account{Groups: []*service.Group{group}}))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(raw), `"rate_multiplier":9`)
|
||||
require.Contains(t, string(raw), `"subscription_type":"subscription"`)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type openCodeGoUsageAutoRefreshRequest struct {
|
||||
Enabled *bool `json:"enabled" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *AccountHandler) GetOpenCodeGoUsageSettings(c *gin.Context) {
|
||||
if h.opencodeGoUsage == nil {
|
||||
response.ErrorFrom(c, service.ErrOpenCodeGoUsageUnavailable)
|
||||
return
|
||||
}
|
||||
settings, err := h.opencodeGoUsage.GetSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, settings)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) UpdateOpenCodeGoUsageSettings(c *gin.Context) {
|
||||
if h.opencodeGoUsage == nil {
|
||||
response.ErrorFrom(c, service.ErrOpenCodeGoUsageUnavailable)
|
||||
return
|
||||
}
|
||||
var req service.OpenCodeGoUsageSettings
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.opencodeGoUsage.UpdateSettings(c.Request.Context(), &req); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
settings, err := h.opencodeGoUsage.GetSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, settings)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) GetOpenCodeGoUsage(c *gin.Context) {
|
||||
if !h.requireOpenCodeGoUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := openCodeGoUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
state, err := h.opencodeGoUsage.GetState(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) SetOpenCodeGoUsageAutoRefresh(c *gin.Context) {
|
||||
if !h.requireOpenCodeGoUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := openCodeGoUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req openCodeGoUsageAutoRefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
state, err := h.opencodeGoUsage.SetAutoRefresh(c.Request.Context(), accountID, *req.Enabled)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) RefreshOpenCodeGoUsage(c *gin.Context) {
|
||||
if !h.requireOpenCodeGoUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := openCodeGoUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
state, err := h.opencodeGoUsage.Refresh(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) requireOpenCodeGoUsage(c *gin.Context) bool {
|
||||
if h != nil && h.opencodeGoUsage != nil {
|
||||
return true
|
||||
}
|
||||
response.ErrorFrom(c, service.ErrOpenCodeGoUsageUnavailable)
|
||||
return false
|
||||
}
|
||||
|
||||
func openCodeGoUsageAccountID(c *gin.Context) (int64, bool) {
|
||||
if c == nil {
|
||||
return 0, false
|
||||
}
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil || accountID <= 0 {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return 0, false
|
||||
}
|
||||
return accountID, true
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type openCodeGoUsageHandlerTestRepo struct {
|
||||
service.AccountRepository
|
||||
account *service.Account
|
||||
accounts []*service.Account
|
||||
}
|
||||
|
||||
func (r *openCodeGoUsageHandlerTestRepo) GetByID(_ context.Context, id int64) (*service.Account, error) {
|
||||
if r.account != nil && r.account.ID == id {
|
||||
return r.account, nil
|
||||
}
|
||||
for _, account := range r.accounts {
|
||||
if account.ID == id {
|
||||
return account, nil
|
||||
}
|
||||
}
|
||||
return nil, service.ErrAccountNotFound
|
||||
}
|
||||
|
||||
func (r *openCodeGoUsageHandlerTestRepo) ListOpenCodeGoUsageGroupAccounts(_ context.Context, anchors []*service.Account) ([]service.Account, error) {
|
||||
wanted := make(map[string]struct{}, len(anchors))
|
||||
for _, anchor := range anchors {
|
||||
if apiKey, ok := openCodeGoUsageHandlerTestAPIKey(anchor); ok {
|
||||
wanted[apiKey] = struct{}{}
|
||||
}
|
||||
}
|
||||
result := make([]service.Account, 0, len(r.accounts)+1)
|
||||
if r.account != nil {
|
||||
if apiKey, ok := openCodeGoUsageHandlerTestAPIKey(r.account); ok {
|
||||
if _, match := wanted[apiKey]; match {
|
||||
result = append(result, *r.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, account := range r.accounts {
|
||||
if apiKey, ok := openCodeGoUsageHandlerTestAPIKey(account); ok {
|
||||
if _, match := wanted[apiKey]; match {
|
||||
result = append(result, *account)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func openCodeGoUsageHandlerTestAPIKey(account *service.Account) (string, bool) {
|
||||
if account == nil || account.Credentials == nil {
|
||||
return "", false
|
||||
}
|
||||
apiKey, ok := account.Credentials["api_key"].(string)
|
||||
return apiKey, ok && apiKey != ""
|
||||
}
|
||||
|
||||
func (r *openCodeGoUsageHandlerTestRepo) SetOpenCodeGoUsageAutoRefresh(context.Context, *service.Account, bool) error {
|
||||
return nil
|
||||
}
|
||||
func (r *openCodeGoUsageHandlerTestRepo) UpdateOpenCodeGoUsageSnapshot(context.Context, *service.Account, *service.OpenCodeGoUsageSnapshot) error {
|
||||
return nil
|
||||
}
|
||||
func (r *openCodeGoUsageHandlerTestRepo) DisableOpenCodeGoUsageAutoRefresh(context.Context, *service.Account) error {
|
||||
return nil
|
||||
}
|
||||
func (r *openCodeGoUsageHandlerTestRepo) ListDueOpenCodeGoUsageAccounts(context.Context, time.Time, time.Duration, time.Duration, int) ([]service.Account, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func newOpenCodeGoUsageHandlerTestService(t *testing.T) *service.OpenCodeGoUsageService {
|
||||
t.Helper()
|
||||
svc := service.NewOpenCodeGoUsageService(nil, nil, nil)
|
||||
t.Cleanup(svc.Stop)
|
||||
return svc
|
||||
}
|
||||
|
||||
func newOpenCodeGoUsageHandlerContext(method, target, body, id string) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(method, target, bytes.NewBufferString(body))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = request
|
||||
if id != "" {
|
||||
ctx.Params = gin.Params{{Key: "id", Value: id}}
|
||||
}
|
||||
return ctx, recorder
|
||||
}
|
||||
|
||||
func TestOpenCodeGoUsageHandlersValidateRequestsAndDependencies(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svc := newOpenCodeGoUsageHandlerTestService(t)
|
||||
|
||||
t.Run("invalid account id", func(t *testing.T) {
|
||||
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodGet, "/admin/accounts/not-an-id/opencode-go-usage", "", "not-an-id")
|
||||
(&AccountHandler{opencodeGoUsage: svc}).GetOpenCodeGoUsage(ctx)
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
|
||||
t.Run("missing enabled", func(t *testing.T) {
|
||||
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodPut, "/admin/accounts/7/opencode-go-usage/auto-refresh", `{}`, "7")
|
||||
(&AccountHandler{opencodeGoUsage: svc}).SetOpenCodeGoUsageAutoRefresh(ctx)
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
|
||||
t.Run("service unavailable", func(t *testing.T) {
|
||||
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodGet, "/admin/accounts/7/opencode-go-usage", "", "7")
|
||||
(&AccountHandler{}).GetOpenCodeGoUsage(ctx)
|
||||
require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), "OPENCODE_GO_USAGE_UNAVAILABLE")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOpenCodeGoUsageSettingsHandlerSuccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
ctx, recorder := newOpenCodeGoUsageHandlerContext(http.MethodGet, "/admin/accounts/opencode-go-usage/settings", "", "")
|
||||
handler := &AccountHandler{opencodeGoUsage: newOpenCodeGoUsageHandlerTestService(t)}
|
||||
|
||||
handler.GetOpenCodeGoUsageSettings(ctx)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), `"enabled":false`)
|
||||
require.Contains(t, recorder.Body.String(), `"interval_minutes":15`)
|
||||
}
|
||||
|
||||
func TestOpenCodeGoUsageStateEmbeddedInListAndDetail(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
now := time.Now().UTC()
|
||||
account := &service.Account{
|
||||
ID: 7, Name: "opencode", Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey,
|
||||
Credentials: map[string]any{"base_url": "https://opencode.ai/zen/go/v1", "api_key": "test-key"},
|
||||
Extra: map[string]any{
|
||||
service.OpenCodeGoUsageAutoRefreshExtraKey: true,
|
||||
service.OpenCodeGoUsageSnapshotExtraKey: &service.OpenCodeGoUsageSnapshot{
|
||||
Status: service.OpenCodeGoUsageStatusOK, Data: &service.OpenCodeGoUsageData{
|
||||
Rolling: service.OpenCodeGoUsageWindow{Status: "ok", Percent: 6},
|
||||
},
|
||||
LastAttemptAt: now, NextRefreshAt: now.Add(time.Hour),
|
||||
},
|
||||
},
|
||||
Status: service.StatusActive,
|
||||
}
|
||||
repo := &openCodeGoUsageHandlerTestRepo{account: account}
|
||||
adminService := newStubAdminService()
|
||||
adminService.accounts = []service.Account{*account}
|
||||
adminService.getAccountResult = account
|
||||
usageService := service.NewOpenCodeGoUsageService(repo, nil, nil)
|
||||
t.Cleanup(usageService.Stop)
|
||||
handler := NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
handler.SetOpenCodeGoUsageService(usageService)
|
||||
router := gin.New()
|
||||
router.GET("/accounts", handler.List)
|
||||
router.GET("/accounts/:id", handler.GetByID)
|
||||
router.GET("/accounts/:id/opencode-go-usage", handler.GetOpenCodeGoUsage)
|
||||
|
||||
listRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(listRecorder, httptest.NewRequest(http.MethodGet, "/accounts?page=1&page_size=20", nil))
|
||||
require.Equal(t, http.StatusOK, listRecorder.Code)
|
||||
var listPayload struct {
|
||||
Data struct {
|
||||
Items []struct {
|
||||
OpenCodeGoUsage *service.OpenCodeGoUsageState `json:"opencode_go_usage"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(listRecorder.Body.Bytes(), &listPayload))
|
||||
require.Len(t, listPayload.Data.Items, 1)
|
||||
require.NotNil(t, listPayload.Data.Items[0].OpenCodeGoUsage)
|
||||
require.True(t, listPayload.Data.Items[0].OpenCodeGoUsage.AutoRefreshEnabled)
|
||||
require.Equal(t, 6.0, listPayload.Data.Items[0].OpenCodeGoUsage.Snapshot.Data.Rolling.Percent)
|
||||
|
||||
detailRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(detailRecorder, httptest.NewRequest(http.MethodGet, "/accounts/7", nil))
|
||||
require.Equal(t, http.StatusOK, detailRecorder.Code)
|
||||
var detailPayload struct {
|
||||
Data struct {
|
||||
OpenCodeGoUsage *service.OpenCodeGoUsageState `json:"opencode_go_usage"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(detailRecorder.Body.Bytes(), &detailPayload))
|
||||
require.NotNil(t, detailPayload.Data.OpenCodeGoUsage)
|
||||
require.Equal(t, 6.0, detailPayload.Data.OpenCodeGoUsage.Snapshot.Data.Rolling.Percent)
|
||||
|
||||
stateRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(stateRecorder, httptest.NewRequest(http.MethodGet, "/accounts/7/opencode-go-usage", nil))
|
||||
require.Equal(t, http.StatusOK, stateRecorder.Code)
|
||||
var statePayload struct {
|
||||
Data service.OpenCodeGoUsageState `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(stateRecorder.Body.Bytes(), &statePayload))
|
||||
require.Equal(t, statePayload.Data.Snapshot, detailPayload.Data.OpenCodeGoUsage.Snapshot)
|
||||
|
||||
for _, body := range []string{listRecorder.Body.String(), detailRecorder.Body.String(), stateRecorder.Body.String()} {
|
||||
require.NotContains(t, body, "test-key")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// upstreamBillingRatesResponse is deliberately smaller than the regular
|
||||
// account-list response. The frontend uses the returned ID order to detect a
|
||||
// page boundary/order change and only applies snapshots when the page is
|
||||
// otherwise stable.
|
||||
type upstreamBillingRatesResponse struct {
|
||||
Items []service.UpstreamBillingRateSnapshotItem `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// GetUpstreamBillingRates returns persisted upstream billing snapshots for the
|
||||
// current account-list page. It never probes an upstream and never loads
|
||||
// today-stats, usage, concurrency, or credentials for the response.
|
||||
//
|
||||
// GET /api/v1/admin/accounts/upstream-billing-rates
|
||||
func (h *AccountHandler) GetUpstreamBillingRates(c *gin.Context) {
|
||||
if h.adminService == nil {
|
||||
response.Error(c, http.StatusServiceUnavailable, "account service unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
page, pageSize := response.ParsePagination(c)
|
||||
platform := c.Query("platform")
|
||||
accountType := c.Query("type")
|
||||
status := c.Query("status")
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
if len(search) > 100 {
|
||||
search = search[:100]
|
||||
}
|
||||
privacyMode := strings.TrimSpace(c.Query("privacy_mode"))
|
||||
sortBy := c.DefaultQuery("sort_by", "name")
|
||||
sortOrder := c.DefaultQuery("sort_order", "asc")
|
||||
|
||||
var groupID int64
|
||||
if groupQuery := c.Query("group"); groupQuery != "" {
|
||||
if groupQuery == accountListGroupUngroupedQueryValue {
|
||||
groupID = service.AccountListGroupUngrouped
|
||||
} else {
|
||||
parsed, err := strconv.ParseInt(groupQuery, 10, 64)
|
||||
if err != nil || parsed < 0 {
|
||||
response.BadRequest(c, "invalid group filter")
|
||||
return
|
||||
}
|
||||
groupID = parsed
|
||||
}
|
||||
}
|
||||
|
||||
accounts, total, err := h.adminService.ListAccounts(
|
||||
c.Request.Context(), page, pageSize, platform, accountType, status,
|
||||
search, groupID, privacyMode, sortBy, sortOrder,
|
||||
)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := upstreamBillingRatesResponse{
|
||||
Items: service.BuildUpstreamBillingRateSnapshotItems(accounts),
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
c.Header("Cache-Control", "private, no-cache")
|
||||
etag := buildUpstreamBillingRatesETag(payload)
|
||||
if etag != "" {
|
||||
c.Header("ETag", etag)
|
||||
if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, payload)
|
||||
}
|
||||
|
||||
func buildUpstreamBillingRatesETag(payload upstreamBillingRatesResponse) string {
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
return "\"" + hex.EncodeToString(sum[:]) + "\""
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
|
||||
|
||||
router.GET("/api/v1/admin/groups", groupHandler.List)
|
||||
router.GET("/api/v1/admin/groups/all", groupHandler.GetAll)
|
||||
router.GET("/api/v1/admin/groups/:id/models-list-candidates", groupHandler.GetModelsListCandidates)
|
||||
router.GET("/api/v1/admin/groups/:id/model-allowlist-candidates", groupHandler.GetGroupModelAllowlistCandidates)
|
||||
router.GET("/api/v1/admin/groups/:id/composite-routes", groupHandler.ListCompositeRoutes)
|
||||
router.POST("/api/v1/admin/groups/:id/composite-routes", groupHandler.CreateCompositeRoute)
|
||||
router.POST("/api/v1/admin/groups/:id/composite-routes/preview", groupHandler.PreviewCompositeRoute)
|
||||
@@ -184,7 +184,7 @@ func TestGroupHandlerEndpoints(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/0/models-list-candidates?platform=openai", nil)
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/0/model-allowlist-candidates?platform=openai", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "gpt-5.5")
|
||||
|
||||
@@ -278,13 +278,15 @@ func TestOpenAIFastPolicySettingsFromDTO_NormalizesServiceTier(t *testing.T) {
|
||||
Rules: []dto.OpenAIFastPolicyRule{
|
||||
{ServiceTier: "priority", Action: "filter", Scope: "all"},
|
||||
{ServiceTier: "flex", Action: "block", Scope: "oauth"},
|
||||
{ServiceTier: "ultrafast", Action: "pass", Scope: "all"},
|
||||
{ServiceTier: "all", Action: "pass", Scope: "apikey"},
|
||||
},
|
||||
}
|
||||
out := openaiFastPolicySettingsFromDTO(in)
|
||||
require.Len(t, out.Rules, 3)
|
||||
require.Len(t, out.Rules, 4)
|
||||
require.Equal(t, service.OpenAIFastTierPriority, out.Rules[0].ServiceTier)
|
||||
require.Equal(t, service.OpenAIFastTierFlex, out.Rules[1].ServiceTier)
|
||||
require.Equal(t, service.OpenAIFastTierAny, out.Rules[2].ServiceTier)
|
||||
require.Equal(t, service.OpenAIFastTierUltrafast, out.Rules[2].ServiceTier)
|
||||
require.Equal(t, service.OpenAIFastTierAny, out.Rules[3].ServiceTier)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ type stubAdminService struct {
|
||||
boundAuthIdentity *service.AdminBindAuthIdentityInput
|
||||
boundAuthIdentityFor int64
|
||||
createdAccounts []*service.CreateAccountInput
|
||||
createdGroups []*service.CreateGroupInput
|
||||
updatedGroups []*service.UpdateGroupInput
|
||||
deletedGroupIDs []int64
|
||||
guardedDeletedGroupIDs []int64
|
||||
deleteGroupIfEmptyErr error
|
||||
advancedGroupOperationCalls int
|
||||
lastListGroupsIsExclusive *bool
|
||||
createdProxies []*service.CreateProxyInput
|
||||
updatedProxyIDs []int64
|
||||
updatedProxies []*service.UpdateProxyInput
|
||||
@@ -267,6 +274,7 @@ func (s *stubAdminService) BindUserAuthIdentity(ctx context.Context, userID int6
|
||||
}
|
||||
|
||||
func (s *stubAdminService) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]service.Group, int64, error) {
|
||||
s.lastListGroupsIsExclusive = isExclusive
|
||||
return s.groups, int64(len(s.groups)), nil
|
||||
}
|
||||
|
||||
@@ -295,6 +303,7 @@ func (s *stubAdminService) GetGroupModelsListCandidates(ctx context.Context, id
|
||||
}
|
||||
|
||||
func (s *stubAdminService) ListCompositeRoutes(ctx context.Context, groupID int64) ([]service.CompositeModelRoute, error) {
|
||||
s.advancedGroupOperationCalls++
|
||||
return []service.CompositeModelRoute{
|
||||
{
|
||||
ID: 1,
|
||||
@@ -311,6 +320,7 @@ func (s *stubAdminService) ListCompositeRoutes(ctx context.Context, groupID int6
|
||||
}
|
||||
|
||||
func (s *stubAdminService) CreateCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
|
||||
s.advancedGroupOperationCalls++
|
||||
return &service.CompositeModelRoute{
|
||||
ID: 1,
|
||||
GroupID: groupID,
|
||||
@@ -326,6 +336,7 @@ func (s *stubAdminService) CreateCompositeRoute(ctx context.Context, groupID int
|
||||
}
|
||||
|
||||
func (s *stubAdminService) UpdateCompositeRoute(ctx context.Context, groupID, routeID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
|
||||
s.advancedGroupOperationCalls++
|
||||
return &service.CompositeModelRoute{
|
||||
ID: routeID,
|
||||
GroupID: groupID,
|
||||
@@ -341,10 +352,12 @@ func (s *stubAdminService) UpdateCompositeRoute(ctx context.Context, groupID, ro
|
||||
}
|
||||
|
||||
func (s *stubAdminService) DeleteCompositeRoute(ctx context.Context, groupID, routeID int64) error {
|
||||
s.advancedGroupOperationCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) PreviewCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRoutePreviewRequest) (*service.CompositeRouteDecision, error) {
|
||||
s.advancedGroupOperationCalls++
|
||||
decision, err := service.NewCompositeRouteResolver(nil).Resolve(ctx, groupID, input.Model, input.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -353,11 +366,13 @@ func (s *stubAdminService) PreviewCompositeRoute(ctx context.Context, groupID in
|
||||
}
|
||||
|
||||
func (s *stubAdminService) CreateGroup(ctx context.Context, input *service.CreateGroupInput) (*service.Group, error) {
|
||||
s.createdGroups = append(s.createdGroups, input)
|
||||
group := service.Group{ID: 200, Name: input.Name, Status: service.StatusActive}
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) DuplicateGroup(ctx context.Context, id int64, actorScope, operationKey string) (*service.Group, error) {
|
||||
s.advancedGroupOperationCalls++
|
||||
group := service.Group{ID: 201, Name: "group (Copy)", Status: "inactive"}
|
||||
return &group, nil
|
||||
}
|
||||
@@ -367,14 +382,21 @@ func (s *stubAdminService) RecoverDuplicateGroup(ctx context.Context, id int64,
|
||||
}
|
||||
|
||||
func (s *stubAdminService) UpdateGroup(ctx context.Context, id int64, input *service.UpdateGroupInput) (*service.Group, error) {
|
||||
s.updatedGroups = append(s.updatedGroups, input)
|
||||
group := service.Group{ID: id, Name: input.Name, Status: service.StatusActive}
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) DeleteGroup(ctx context.Context, id int64) error {
|
||||
s.deletedGroupIDs = append(s.deletedGroupIDs, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) DeleteGroupIfEmpty(ctx context.Context, id int64) error {
|
||||
s.guardedDeletedGroupIDs = append(s.guardedDeletedGroupIDs, id)
|
||||
return s.deleteGroupIfEmptyErr
|
||||
}
|
||||
|
||||
func (s *stubAdminService) GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]service.APIKey, int64, error) {
|
||||
return s.apiKeys, int64(len(s.apiKeys)), nil
|
||||
}
|
||||
@@ -384,10 +406,12 @@ func (s *stubAdminService) GetGroupRateMultipliers(_ context.Context, _ int64) (
|
||||
}
|
||||
|
||||
func (s *stubAdminService) ClearGroupRateMultipliers(_ context.Context, _ int64) error {
|
||||
s.advancedGroupOperationCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) BatchSetGroupRateMultipliers(_ context.Context, _ int64, _ []service.GroupRateMultiplierInput) error {
|
||||
s.advancedGroupOperationCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -396,6 +420,7 @@ func (s *stubAdminService) ClearGroupRPMOverrides(_ context.Context, _ int64) er
|
||||
}
|
||||
|
||||
func (s *stubAdminService) BatchSetGroupRPMOverrides(_ context.Context, _ int64, _ []service.GroupRPMOverrideInput) error {
|
||||
s.advancedGroupOperationCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -491,6 +516,8 @@ func (s *stubAdminService) CreateAccount(ctx context.Context, input *service.Cre
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) ValidateAccountGroupBindings(context.Context, []int64) error { return nil }
|
||||
|
||||
func (s *stubAdminService) DuplicateAccount(ctx context.Context, id int64, actorScope, operationKey string) (*service.Account, error) {
|
||||
account := service.Account{ID: 301, Name: "account (Copy)", Status: service.StatusActive, Schedulable: false}
|
||||
return &account, nil
|
||||
|
||||
@@ -201,6 +201,42 @@ func (h *AffiliateHandler) GetUserOverview(c *gin.Context) {
|
||||
response.Success(c, overview)
|
||||
}
|
||||
|
||||
// WithdrawQuotaRequest 是登记线下提现的请求体。Amount 是已在站外打款给
|
||||
// 用户的金额,由 AffiliateService 校验。
|
||||
type WithdrawQuotaRequest struct {
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
// WithdrawQuota records an offline withdrawal paid outside the site and
|
||||
// deducts it from the user's available affiliate quota. The required
|
||||
// Idempotency-Key header identifies one registration: a repeated request with
|
||||
// the same key deducts nothing and returns the first result with
|
||||
// X-Idempotency-Replayed: true.
|
||||
// POST /api/v1/admin/affiliates/users/:user_id/withdraw
|
||||
func (h *AffiliateHandler) WithdrawQuota(c *gin.Context) {
|
||||
userID, err := strconv.ParseInt(c.Param("user_id"), 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
response.BadRequest(c, "Invalid user_id")
|
||||
return
|
||||
}
|
||||
|
||||
var req WithdrawQuotaRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.affiliateService.AdminWithdrawQuota(c.Request.Context(), userID, req.Amount, c.GetHeader("Idempotency-Key"))
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if result != nil && result.Replayed {
|
||||
c.Header("X-Idempotency-Replayed", "true")
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// ListInviteRecords returns all inviter-invitee relationships.
|
||||
// GET /api/v1/admin/affiliates/invites
|
||||
func (h *AffiliateHandler) ListInviteRecords(c *gin.Context) {
|
||||
@@ -214,7 +250,8 @@ func (h *AffiliateHandler) ListInviteRecords(c *gin.Context) {
|
||||
response.Paginated(c, items, total, filter.Page, filter.PageSize)
|
||||
}
|
||||
|
||||
// ListRebateRecords returns all order-level affiliate rebate records.
|
||||
// ListRebateRecords returns every affiliate rebate accrual, with order details
|
||||
// when the rebate came from a payment order.
|
||||
// GET /api/v1/admin/affiliates/rebates
|
||||
func (h *AffiliateHandler) ListRebateRecords(c *gin.Context) {
|
||||
page, pageSize := response.ParsePagination(c)
|
||||
@@ -227,7 +264,8 @@ func (h *AffiliateHandler) ListRebateRecords(c *gin.Context) {
|
||||
response.Paginated(c, items, total, filter.Page, filter.PageSize)
|
||||
}
|
||||
|
||||
// ListTransferRecords returns all affiliate quota-to-balance transfer records.
|
||||
// ListTransferRecords returns affiliate quota outflows: user transfers into
|
||||
// balance and admin-recorded offline withdrawals.
|
||||
// GET /api/v1/admin/affiliates/transfers
|
||||
func (h *AffiliateHandler) ListTransferRecords(c *gin.Context) {
|
||||
page, pageSize := response.ParsePagination(c)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testWithdrawIdempotencyKey = "affiliate-withdraw-1-5d0c7a2e"
|
||||
|
||||
type withdrawQuotaCall struct {
|
||||
userID int64
|
||||
amount float64
|
||||
operationID string
|
||||
}
|
||||
|
||||
type withdrawQuotaRepoStub struct {
|
||||
service.AffiliateRepository
|
||||
calls []withdrawQuotaCall
|
||||
result *service.AffiliateWithdrawResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *withdrawQuotaRepoStub) WithdrawQuota(_ context.Context, userID int64, amount float64, operationID string) (*service.AffiliateWithdrawResult, error) {
|
||||
s.calls = append(s.calls, withdrawQuotaCall{userID: userID, amount: amount, operationID: operationID})
|
||||
return s.result, s.err
|
||||
}
|
||||
|
||||
type withdrawQuotaResponse struct {
|
||||
Code int `json:"code"`
|
||||
Reason string `json:"reason"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func performWithdrawQuota(t *testing.T, repo *withdrawQuotaRepoStub, userID, idempotencyKey, body string) (*httptest.ResponseRecorder, withdrawQuotaResponse) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
handler := NewAffiliateHandler(service.NewAffiliateService(repo, nil, nil, nil), nil)
|
||||
router.POST("/api/v1/admin/affiliates/users/:user_id/withdraw", handler.WithdrawQuota)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/affiliates/users/"+userID+"/withdraw", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if idempotencyKey != "" {
|
||||
req.Header.Set("Idempotency-Key", idempotencyKey)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp withdrawQuotaResponse
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
return rec, resp
|
||||
}
|
||||
|
||||
// TestAffiliateHandlerWithdrawQuota_Success 验证登记线下提现把路径中的用户、
|
||||
// 舍入后的金额与幂等键派生的 operation_id 交给仓储,并返回登记结果。
|
||||
func TestAffiliateHandlerWithdrawQuota_Success(t *testing.T) {
|
||||
repo := &withdrawQuotaRepoStub{result: &service.AffiliateWithdrawResult{LedgerID: 9, UserID: 42, Amount: 12.34567891}}
|
||||
rec, resp := performWithdrawQuota(t, repo, "42", testWithdrawIdempotencyKey, `{"amount":12.3456789149}`)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, 0, resp.Code)
|
||||
var data service.AffiliateWithdrawResult
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &data))
|
||||
require.Equal(t, int64(9), data.LedgerID)
|
||||
require.Empty(t, rec.Header().Get("X-Idempotency-Replayed"))
|
||||
|
||||
require.Len(t, repo.calls, 1)
|
||||
require.Equal(t, int64(42), repo.calls[0].userID)
|
||||
require.Equal(t, 12.34567891, repo.calls[0].amount)
|
||||
require.Regexp(t, `^[0-9a-f]{64}$`, repo.calls[0].operationID)
|
||||
}
|
||||
|
||||
// TestAffiliateHandlerWithdrawQuota_SameKeyMapsToSameOperation 验证重试时带同一个
|
||||
// Idempotency-Key 的请求落到同一个 operation_id,换一个键则是另一笔登记。
|
||||
func TestAffiliateHandlerWithdrawQuota_SameKeyMapsToSameOperation(t *testing.T) {
|
||||
repo := &withdrawQuotaRepoStub{result: &service.AffiliateWithdrawResult{LedgerID: 9, UserID: 42, Amount: 10}}
|
||||
for _, key := range []string{testWithdrawIdempotencyKey, testWithdrawIdempotencyKey, testWithdrawIdempotencyKey + "-next"} {
|
||||
rec, _ := performWithdrawQuota(t, repo, "42", key, `{"amount":10}`)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
require.Len(t, repo.calls, 3)
|
||||
require.Equal(t, repo.calls[0].operationID, repo.calls[1].operationID)
|
||||
require.NotEqual(t, repo.calls[0].operationID, repo.calls[2].operationID)
|
||||
}
|
||||
|
||||
// TestAffiliateHandlerWithdrawQuota_ReplayReturnsFirstResult 验证命中已有登记时
|
||||
// 原样返回首次登记的结果,并以 X-Idempotency-Replayed 标明是重放。
|
||||
func TestAffiliateHandlerWithdrawQuota_ReplayReturnsFirstResult(t *testing.T) {
|
||||
repo := &withdrawQuotaRepoStub{result: &service.AffiliateWithdrawResult{
|
||||
LedgerID: 9,
|
||||
UserID: 42,
|
||||
Amount: 10,
|
||||
AvailableQuotaAfter: 90,
|
||||
HistoryQuotaAfter: 100,
|
||||
Replayed: true,
|
||||
}}
|
||||
rec, resp := performWithdrawQuota(t, repo, "42", testWithdrawIdempotencyKey, `{"amount":10}`)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "true", rec.Header().Get("X-Idempotency-Replayed"))
|
||||
require.JSONEq(t, `{
|
||||
"ledger_id": 9,
|
||||
"user_id": 42,
|
||||
"amount": 10,
|
||||
"available_quota_after": 90,
|
||||
"frozen_quota_after": 0,
|
||||
"history_quota_after": 100
|
||||
}`, string(resp.Data))
|
||||
}
|
||||
|
||||
// TestAffiliateHandlerWithdrawQuota_RejectsBadRequests 验证非法用户 ID、非法 JSON、
|
||||
// 缺少或非法的幂等键与非法金额在进入仓储前以 400 拒绝。
|
||||
func TestAffiliateHandlerWithdrawQuota_RejectsBadRequests(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
userID string
|
||||
key string
|
||||
body string
|
||||
wantReason string
|
||||
}{
|
||||
{name: "non-numeric user id", userID: "abc", key: testWithdrawIdempotencyKey, body: `{"amount":1}`},
|
||||
{name: "non-positive user id", userID: "0", key: testWithdrawIdempotencyKey, body: `{"amount":1}`},
|
||||
{name: "malformed json", userID: "42", key: testWithdrawIdempotencyKey, body: `{"amount":`},
|
||||
{name: "missing idempotency key", userID: "42", body: `{"amount":1}`, wantReason: "IDEMPOTENCY_KEY_REQUIRED"},
|
||||
{name: "invalid idempotency key", userID: "42", key: "has space", body: `{"amount":1}`, wantReason: "IDEMPOTENCY_KEY_INVALID"},
|
||||
{name: "zero amount", userID: "42", key: testWithdrawIdempotencyKey, body: `{"amount":0}`, wantReason: "AFFILIATE_WITHDRAW_AMOUNT_INVALID"},
|
||||
{name: "missing amount", userID: "42", key: testWithdrawIdempotencyKey, body: `{}`, wantReason: "AFFILIATE_WITHDRAW_AMOUNT_INVALID"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
repo := &withdrawQuotaRepoStub{}
|
||||
rec, resp := performWithdrawQuota(t, repo, tc.userID, tc.key, tc.body)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
require.Equal(t, tc.wantReason, resp.Reason)
|
||||
require.Empty(t, repo.calls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAffiliateHandlerWithdrawQuota_RepositoryRejections 验证额度不足以 400、
|
||||
// 同一幂等键换了用户或金额以 409 返回,前端据错误码提示。
|
||||
func TestAffiliateHandlerWithdrawQuota_RepositoryRejections(t *testing.T) {
|
||||
cases := []struct {
|
||||
err error
|
||||
wantStatus int
|
||||
wantReason string
|
||||
}{
|
||||
{err: service.ErrAffiliateQuotaInsufficient, wantStatus: http.StatusBadRequest, wantReason: "AFFILIATE_QUOTA_INSUFFICIENT"},
|
||||
{err: service.ErrIdempotencyKeyConflict, wantStatus: http.StatusConflict, wantReason: "IDEMPOTENCY_KEY_CONFLICT"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.wantReason, func(t *testing.T) {
|
||||
repo := &withdrawQuotaRepoStub{err: tc.err}
|
||||
rec, resp := performWithdrawQuota(t, repo, "42", testWithdrawIdempotencyKey, `{"amount":5}`)
|
||||
|
||||
require.Equal(t, tc.wantStatus, rec.Code)
|
||||
require.Equal(t, tc.wantReason, resp.Reason)
|
||||
require.Empty(t, rec.Header().Get("X-Idempotency-Replayed"))
|
||||
require.Len(t, repo.calls, 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,13 @@ func (h *BackupHandler) DeleteBackup(c *gin.Context) {
|
||||
response.BadRequest(c, "backup ID is required")
|
||||
return
|
||||
}
|
||||
if err := h.backupService.DeleteBackup(c.Request.Context(), backupID); err != nil {
|
||||
var err error
|
||||
if c.Query("delete_archived") == "true" {
|
||||
err = h.backupService.DeleteArchivedBackup(c.Request.Context(), backupID)
|
||||
} else {
|
||||
err = h.backupService.DeleteBackup(c.Request.Context(), backupID)
|
||||
}
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,20 +57,22 @@ type updateChannelRequest struct {
|
||||
}
|
||||
|
||||
type channelModelPricingRequest struct {
|
||||
Platform string `json:"platform" binding:"omitempty,max=50"`
|
||||
Models []string `json:"models" binding:"required,min=1,max=100"`
|
||||
BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"`
|
||||
InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"`
|
||||
OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"`
|
||||
FastMultiplier *float64 `json:"fast_multiplier" binding:"omitempty,gt=0"`
|
||||
FlexMultiplier *float64 `json:"flex_multiplier" binding:"omitempty,gt=0"`
|
||||
ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"`
|
||||
ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"`
|
||||
PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"`
|
||||
Intervals []pricingIntervalRequest `json:"intervals"`
|
||||
TimePricing *channelTimePricingRequest `json:"time_pricing"`
|
||||
Platform string `json:"platform" binding:"omitempty,max=50"`
|
||||
Models []string `json:"models" binding:"required,min=1,max=100"`
|
||||
BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"`
|
||||
InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"`
|
||||
OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"`
|
||||
CacheWrite1hPrice *float64 `json:"cache_write_1h_price" binding:"omitempty,min=0"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"`
|
||||
FastMultiplier *float64 `json:"fast_multiplier" binding:"omitempty,gt=0"`
|
||||
FlexMultiplier *float64 `json:"flex_multiplier" binding:"omitempty,gt=0"`
|
||||
ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers"`
|
||||
ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"`
|
||||
ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"`
|
||||
PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"`
|
||||
Intervals []pricingIntervalRequest `json:"intervals"`
|
||||
TimePricing *channelTimePricingRequest `json:"time_pricing"`
|
||||
}
|
||||
|
||||
type channelTimePricingRequest struct {
|
||||
@@ -92,6 +94,7 @@ type pricingIntervalRequest struct {
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price"`
|
||||
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
InputMultiplier *float64 `json:"input_multiplier" binding:"omitempty,gt=0"`
|
||||
OutputMultiplier *float64 `json:"output_multiplier" binding:"omitempty,gt=0"`
|
||||
@@ -127,21 +130,23 @@ type channelResponse struct {
|
||||
}
|
||||
|
||||
type channelModelPricingResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
Models []string `json:"models"`
|
||||
BillingMode string `json:"billing_mode"`
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
FastMultiplier *float64 `json:"fast_multiplier"`
|
||||
FlexMultiplier *float64 `json:"flex_multiplier"`
|
||||
ImageInputPrice *float64 `json:"image_input_price"`
|
||||
ImageOutputPrice *float64 `json:"image_output_price"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
Intervals []pricingIntervalResponse `json:"intervals"`
|
||||
TimePricing *channelTimePricingResponse `json:"time_pricing"`
|
||||
ID int64 `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
Models []string `json:"models"`
|
||||
BillingMode string `json:"billing_mode"`
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price"`
|
||||
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
FastMultiplier *float64 `json:"fast_multiplier"`
|
||||
FlexMultiplier *float64 `json:"flex_multiplier"`
|
||||
ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers"`
|
||||
ImageInputPrice *float64 `json:"image_input_price"`
|
||||
ImageOutputPrice *float64 `json:"image_output_price"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
Intervals []pricingIntervalResponse `json:"intervals"`
|
||||
TimePricing *channelTimePricingResponse `json:"time_pricing"`
|
||||
}
|
||||
|
||||
type channelTimePricingResponse struct {
|
||||
@@ -164,6 +169,7 @@ type pricingIntervalResponse struct {
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price"`
|
||||
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
InputMultiplier *float64 `json:"input_multiplier"`
|
||||
OutputMultiplier *float64 `json:"output_multiplier"`
|
||||
@@ -254,21 +260,23 @@ func pricingToResponse(p *service.ChannelModelPricing) channelModelPricingRespon
|
||||
intervals = append(intervals, intervalToResponse(iv))
|
||||
}
|
||||
return channelModelPricingResponse{
|
||||
ID: p.ID,
|
||||
Platform: platform,
|
||||
Models: models,
|
||||
BillingMode: billingMode,
|
||||
InputPrice: p.InputPrice,
|
||||
OutputPrice: p.OutputPrice,
|
||||
CacheWritePrice: p.CacheWritePrice,
|
||||
CacheReadPrice: p.CacheReadPrice,
|
||||
FastMultiplier: p.FastMultiplier,
|
||||
FlexMultiplier: p.FlexMultiplier,
|
||||
ImageInputPrice: p.ImageInputPrice,
|
||||
ImageOutputPrice: p.ImageOutputPrice,
|
||||
PerRequestPrice: p.PerRequestPrice,
|
||||
Intervals: intervals,
|
||||
TimePricing: timePricingToResponse(p.TimePricing),
|
||||
ID: p.ID,
|
||||
Platform: platform,
|
||||
Models: models,
|
||||
BillingMode: billingMode,
|
||||
InputPrice: p.InputPrice,
|
||||
OutputPrice: p.OutputPrice,
|
||||
CacheWritePrice: p.CacheWritePrice,
|
||||
CacheWrite1hPrice: p.CacheWrite1hPrice,
|
||||
CacheReadPrice: p.CacheReadPrice,
|
||||
FastMultiplier: p.FastMultiplier,
|
||||
FlexMultiplier: p.FlexMultiplier,
|
||||
ReasoningEffortMultipliers: p.ReasoningEffortMultipliers,
|
||||
ImageInputPrice: p.ImageInputPrice,
|
||||
ImageOutputPrice: p.ImageOutputPrice,
|
||||
PerRequestPrice: p.PerRequestPrice,
|
||||
Intervals: intervals,
|
||||
TimePricing: timePricingToResponse(p.TimePricing),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +308,7 @@ func intervalToResponse(iv service.PricingInterval) pricingIntervalResponse {
|
||||
InputPrice: iv.InputPrice,
|
||||
OutputPrice: iv.OutputPrice,
|
||||
CacheWritePrice: iv.CacheWritePrice,
|
||||
CacheWrite1hPrice: iv.CacheWrite1hPrice,
|
||||
CacheReadPrice: iv.CacheReadPrice,
|
||||
InputMultiplier: iv.InputMultiplier,
|
||||
OutputMultiplier: iv.OutputMultiplier,
|
||||
@@ -334,6 +343,7 @@ func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMult
|
||||
InputPrice: iv.InputPrice,
|
||||
OutputPrice: iv.OutputPrice,
|
||||
CacheWritePrice: iv.CacheWritePrice,
|
||||
CacheWrite1hPrice: iv.CacheWrite1hPrice,
|
||||
CacheReadPrice: iv.CacheReadPrice,
|
||||
InputMultiplier: inputMultiplier,
|
||||
OutputMultiplier: outputMultiplier,
|
||||
@@ -349,20 +359,22 @@ func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMult
|
||||
flexMultiplier = r.FlexMultiplier
|
||||
}
|
||||
result = append(result, service.ChannelModelPricing{
|
||||
Platform: platform,
|
||||
Models: r.Models,
|
||||
BillingMode: billingMode,
|
||||
InputPrice: r.InputPrice,
|
||||
OutputPrice: r.OutputPrice,
|
||||
CacheWritePrice: r.CacheWritePrice,
|
||||
CacheReadPrice: r.CacheReadPrice,
|
||||
FastMultiplier: fastMultiplier,
|
||||
FlexMultiplier: flexMultiplier,
|
||||
ImageInputPrice: r.ImageInputPrice,
|
||||
ImageOutputPrice: r.ImageOutputPrice,
|
||||
PerRequestPrice: r.PerRequestPrice,
|
||||
Intervals: intervals,
|
||||
TimePricing: timePricingRequestToService(r.TimePricing),
|
||||
Platform: platform,
|
||||
Models: r.Models,
|
||||
BillingMode: billingMode,
|
||||
InputPrice: r.InputPrice,
|
||||
OutputPrice: r.OutputPrice,
|
||||
CacheWritePrice: r.CacheWritePrice,
|
||||
CacheWrite1hPrice: r.CacheWrite1hPrice,
|
||||
CacheReadPrice: r.CacheReadPrice,
|
||||
FastMultiplier: fastMultiplier,
|
||||
FlexMultiplier: flexMultiplier,
|
||||
ReasoningEffortMultipliers: r.ReasoningEffortMultipliers,
|
||||
ImageInputPrice: r.ImageInputPrice,
|
||||
ImageOutputPrice: r.ImageOutputPrice,
|
||||
PerRequestPrice: r.PerRequestPrice,
|
||||
Intervals: intervals,
|
||||
TimePricing: timePricingRequestToService(r.TimePricing),
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -597,15 +609,25 @@ func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) {
|
||||
response.Success(c, gin.H{"found": false})
|
||||
return
|
||||
}
|
||||
cacheWritePrice := pricing.CacheCreationPricePerToken
|
||||
var cacheWrite1hPrice *float64
|
||||
if pricing.SupportsCacheBreakdown {
|
||||
if pricing.CacheCreation5mPrice > 0 {
|
||||
cacheWritePrice = pricing.CacheCreation5mPrice
|
||||
}
|
||||
cacheWrite1hPrice = &pricing.CacheCreation1hPrice
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"found": true,
|
||||
"input_price": pricing.InputPricePerToken,
|
||||
"output_price": pricing.OutputPricePerToken,
|
||||
"cache_write_price": pricing.CacheCreationPricePerToken,
|
||||
"cache_read_price": pricing.CacheReadPricePerToken,
|
||||
"image_input_price": pricing.ImageInputPricePerToken,
|
||||
"image_output_price": pricing.ImageOutputPricePerToken,
|
||||
"found": true,
|
||||
"input_price": pricing.InputPricePerToken,
|
||||
"output_price": pricing.OutputPricePerToken,
|
||||
"cache_write_price": cacheWritePrice,
|
||||
"cache_write_1h_price": cacheWrite1hPrice,
|
||||
"cache_read_price": pricing.CacheReadPricePerToken,
|
||||
"reasoning_effort_multipliers": pricing.ReasoningEffortMultipliers,
|
||||
"image_input_price": pricing.ImageInputPricePerToken,
|
||||
"image_output_price": pricing.ImageOutputPricePerToken,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -620,6 +642,9 @@ var platformToLiteLLMProvider = map[string]string{
|
||||
service.PlatformKimi: "moonshot",
|
||||
service.PlatformZhipu: "zhipu",
|
||||
service.PlatformDeepseek: "deepseek",
|
||||
service.PlatformMiniMax: "minimax",
|
||||
service.PlatformOpenCodeGo: "opencode-go",
|
||||
service.PlatformTypeSafe: "typesafe",
|
||||
}
|
||||
|
||||
// SyncPricingModels 返回 LiteLLM 定价目录中指定平台的最新模型列表
|
||||
|
||||
@@ -320,15 +320,16 @@ func TestPricingRequestToService_Defaults(t *testing.T) {
|
||||
func TestPricingRequestToService_WithAllFields(t *testing.T) {
|
||||
reqs := []channelModelPricingRequest{
|
||||
{
|
||||
Platform: "openai",
|
||||
Models: []string{"gpt-4", "gpt-4o"},
|
||||
BillingMode: "per_request",
|
||||
InputPrice: float64Ptr(0.01),
|
||||
OutputPrice: float64Ptr(0.03),
|
||||
CacheWritePrice: float64Ptr(0.005),
|
||||
CacheReadPrice: float64Ptr(0.002),
|
||||
ImageOutputPrice: float64Ptr(0.04),
|
||||
PerRequestPrice: float64Ptr(0.5),
|
||||
Platform: "openai",
|
||||
Models: []string{"gpt-4", "gpt-4o"},
|
||||
BillingMode: "per_request",
|
||||
InputPrice: float64Ptr(0.01),
|
||||
OutputPrice: float64Ptr(0.03),
|
||||
CacheWritePrice: float64Ptr(0.005),
|
||||
CacheWrite1hPrice: float64Ptr(0.008),
|
||||
CacheReadPrice: float64Ptr(0.002),
|
||||
ImageOutputPrice: float64Ptr(0.04),
|
||||
PerRequestPrice: float64Ptr(0.5),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -341,6 +342,7 @@ func TestPricingRequestToService_WithAllFields(t *testing.T) {
|
||||
require.Equal(t, float64Ptr(0.01), r.InputPrice)
|
||||
require.Equal(t, float64Ptr(0.03), r.OutputPrice)
|
||||
require.Equal(t, float64Ptr(0.005), r.CacheWritePrice)
|
||||
require.Equal(t, float64Ptr(0.008), r.CacheWrite1hPrice)
|
||||
require.Equal(t, float64Ptr(0.002), r.CacheReadPrice)
|
||||
require.Equal(t, float64Ptr(0.04), r.ImageOutputPrice)
|
||||
require.Equal(t, float64Ptr(0.5), r.PerRequestPrice)
|
||||
@@ -353,15 +355,16 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
|
||||
BillingMode: "per_request",
|
||||
Intervals: []pricingIntervalRequest{
|
||||
{
|
||||
MinTokens: 0,
|
||||
MaxTokens: intPtr(2000),
|
||||
TierLabel: "small",
|
||||
InputPrice: float64Ptr(0.01),
|
||||
OutputPrice: float64Ptr(0.02),
|
||||
CacheWritePrice: float64Ptr(0.003),
|
||||
CacheReadPrice: float64Ptr(0.001),
|
||||
PerRequestPrice: float64Ptr(0.1),
|
||||
SortOrder: 1,
|
||||
MinTokens: 0,
|
||||
MaxTokens: intPtr(2000),
|
||||
TierLabel: "small",
|
||||
InputPrice: float64Ptr(0.01),
|
||||
OutputPrice: float64Ptr(0.02),
|
||||
CacheWritePrice: float64Ptr(0.003),
|
||||
CacheWrite1hPrice: float64Ptr(0.006),
|
||||
CacheReadPrice: float64Ptr(0.001),
|
||||
PerRequestPrice: float64Ptr(0.1),
|
||||
SortOrder: 1,
|
||||
},
|
||||
{
|
||||
MinTokens: 2000,
|
||||
@@ -384,6 +387,7 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
|
||||
require.Equal(t, float64Ptr(0.01), iv0.InputPrice)
|
||||
require.Equal(t, float64Ptr(0.02), iv0.OutputPrice)
|
||||
require.Equal(t, float64Ptr(0.003), iv0.CacheWritePrice)
|
||||
require.Equal(t, float64Ptr(0.006), iv0.CacheWrite1hPrice)
|
||||
require.Equal(t, float64Ptr(0.001), iv0.CacheReadPrice)
|
||||
require.Equal(t, float64Ptr(0.1), iv0.PerRequestPrice)
|
||||
require.Equal(t, 1, iv0.SortOrder)
|
||||
@@ -445,14 +449,14 @@ func TestPricingRequestToService_TimePricingNil(t *testing.T) {
|
||||
require.Nil(t, got[0].TimePricing)
|
||||
}
|
||||
|
||||
// 账号成本统计规则不支持倍率:allowChannelMultipliers=false 时必须丢弃,
|
||||
// 避免渠道倍率意外污染账号成本口径。
|
||||
// Fast/Flex 和区间倍率只用于渠道售价;思考等级倍率可在账号统计规则中独立配置。
|
||||
func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) {
|
||||
req := channelModelPricingRequest{
|
||||
Models: []string{"gpt-5"},
|
||||
BillingMode: "token",
|
||||
FastMultiplier: float64Ptr(2.5),
|
||||
FlexMultiplier: float64Ptr(0.5),
|
||||
Models: []string{"gpt-5"},
|
||||
BillingMode: "token",
|
||||
FastMultiplier: float64Ptr(2.5),
|
||||
FlexMultiplier: float64Ptr(0.5),
|
||||
ReasoningEffortMultipliers: map[string]float64{"high": 1.5, "max": 3},
|
||||
Intervals: []pricingIntervalRequest{{
|
||||
MinTokens: 272000,
|
||||
InputMultiplier: float64Ptr(2),
|
||||
@@ -465,6 +469,7 @@ func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) {
|
||||
allowed := pricingRequestToService([]channelModelPricingRequest{req}, true)
|
||||
require.Equal(t, float64Ptr(2.5), allowed[0].FastMultiplier)
|
||||
require.Equal(t, float64Ptr(0.5), allowed[0].FlexMultiplier)
|
||||
require.Equal(t, req.ReasoningEffortMultipliers, allowed[0].ReasoningEffortMultipliers)
|
||||
require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].InputMultiplier)
|
||||
require.Equal(t, float64Ptr(1.5), allowed[0].Intervals[0].OutputMultiplier)
|
||||
require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].CacheWriteMultiplier)
|
||||
@@ -473,6 +478,7 @@ func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) {
|
||||
dropped := pricingRequestToService([]channelModelPricingRequest{req}, false)
|
||||
require.Nil(t, dropped[0].FastMultiplier)
|
||||
require.Nil(t, dropped[0].FlexMultiplier)
|
||||
require.Equal(t, req.ReasoningEffortMultipliers, dropped[0].ReasoningEffortMultipliers)
|
||||
require.Nil(t, dropped[0].Intervals[0].InputMultiplier)
|
||||
require.Nil(t, dropped[0].Intervals[0].OutputMultiplier)
|
||||
require.Nil(t, dropped[0].Intervals[0].CacheWriteMultiplier)
|
||||
@@ -504,6 +510,23 @@ func TestPricingToResponse_TimePricingNil(t *testing.T) {
|
||||
require.Nil(t, got.TimePricing)
|
||||
}
|
||||
|
||||
func TestPricingRequestAndResponse_ReasoningEffortMultipliers(t *testing.T) {
|
||||
var req channelModelPricingRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"models":["custom-model"],"reasoning_effort_multipliers":{"none":0.5,"high":1.5,"max":3}}`), &req))
|
||||
pricing := pricingRequestToService([]channelModelPricingRequest{req}, true)
|
||||
got := pricingToResponse(&pricing[0])
|
||||
require.Equal(t, map[string]float64{"none": 0.5, "high": 1.5, "max": 3}, got.ReasoningEffortMultipliers)
|
||||
data, err := json.Marshal(got)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(data), `"reasoning_effort_multipliers":{"high":1.5,"max":3,"none":0.5}`)
|
||||
require.NotContains(t, string(data), "max_reasoning_effort_multiplier")
|
||||
|
||||
req = channelModelPricingRequest{}
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"models":["custom-model"],"reasoning_effort_multipliers":{}}`), &req))
|
||||
pricing = pricingRequestToService([]channelModelPricingRequest{req}, true)
|
||||
require.Empty(t, pricing[0].ReasoningEffortMultipliers)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. SyncPricingModels handler
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -542,7 +565,7 @@ func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) {
|
||||
svc := service.NewPricingService(nil, nil)
|
||||
router := setupSyncPricingModelsRouter(svc)
|
||||
|
||||
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity", "grok", "kimi", "zhipu", "deepseek"} {
|
||||
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity", "grok", "kimi", "zhipu", "deepseek", "minimax", "typesafe"} {
|
||||
req := httptest.NewRequest(http.MethodGet, "/channels/pricing/sync-models?platform="+platform, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
@@ -558,3 +581,52 @@ func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) {
|
||||
require.NotNil(t, body.Data.Models, "models must not be null for platform=%s", platform)
|
||||
}
|
||||
}
|
||||
|
||||
func setupModelDefaultPricingRouter() *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
h := &ChannelHandler{billingService: service.NewBillingService(nil, nil)}
|
||||
router.GET("/channels/model-pricing", h.GetModelDefaultPricing)
|
||||
return router
|
||||
}
|
||||
|
||||
func TestGetModelDefaultPricing_ReturnsFable51CacheTTLs(t *testing.T) {
|
||||
router := setupModelDefaultPricingRouter()
|
||||
req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-fable-5-1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var body struct {
|
||||
Data struct {
|
||||
Found bool `json:"found"`
|
||||
CacheWritePrice float64 `json:"cache_write_price"`
|
||||
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
|
||||
ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
||||
require.True(t, body.Data.Found)
|
||||
require.InDelta(t, 12.5e-6, body.Data.CacheWritePrice, 1e-12)
|
||||
require.NotNil(t, body.Data.CacheWrite1hPrice)
|
||||
require.InDelta(t, 20e-6, *body.Data.CacheWrite1hPrice, 1e-12)
|
||||
require.Empty(t, body.Data.ReasoningEffortMultipliers)
|
||||
}
|
||||
|
||||
func TestGetModelDefaultPricing_OmitsUnsupportedCache1hPrice(t *testing.T) {
|
||||
router := setupModelDefaultPricingRouter()
|
||||
req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-sonnet-4", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var body struct {
|
||||
Data struct {
|
||||
Found bool `json:"found"`
|
||||
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
||||
require.True(t, body.Data.Found)
|
||||
require.Nil(t, body.Data.CacheWrite1hPrice)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *Ch
|
||||
|
||||
type channelMonitorCreateRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek"`
|
||||
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek minimax opencode_go"`
|
||||
APIMode string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
|
||||
Endpoint string `json:"endpoint" binding:"omitempty,max=500"`
|
||||
APIKey string `json:"api_key" binding:"omitempty,max=2000"`
|
||||
@@ -64,7 +64,7 @@ type channelMonitorCreateRequest struct {
|
||||
|
||||
type channelMonitorUpdateRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek"`
|
||||
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek minimax opencode_go"`
|
||||
APIMode *string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
|
||||
Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
|
||||
APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type claudeResetReader interface {
|
||||
Query(context.Context, int64) (*service.ClaudeResetCredits, error)
|
||||
Redeem(context.Context, int64, string) (*service.ClaudeResetOutcome, error)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) SetClaudeResetCreditService(s *service.ClaudeResetCreditService) {
|
||||
h.claudeResetCredits = s
|
||||
}
|
||||
func (h *AccountHandler) ClaudeResetCredits(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return
|
||||
}
|
||||
if h.claudeResetCredits == nil {
|
||||
response.Error(c, 503, "Claude reset service unavailable")
|
||||
return
|
||||
}
|
||||
status, err := h.claudeResetCredits.Query(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, status)
|
||||
}
|
||||
|
||||
// RedeemClaudeResetCredit consumes the upstream next reset credit. The request has
|
||||
// no body: the server picks the grant; the Idempotency-Key header identifies one
|
||||
// operator confirmation and replays its outcome.
|
||||
func (h *AccountHandler) RedeemClaudeResetCredit(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return
|
||||
}
|
||||
if h.claudeResetCredits == nil {
|
||||
response.Error(c, 503, "Claude reset service unavailable")
|
||||
return
|
||||
}
|
||||
result, err := h.claudeResetCredits.Redeem(c.Request.Context(), id, c.GetHeader("Idempotency-Key"))
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type claudeResetHandlerStub struct {
|
||||
key string
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *claudeResetHandlerStub) Query(context.Context, int64) (*service.ClaudeResetCredits, error) {
|
||||
return &service.ClaudeResetCredits{Credits: []service.ClaudeResetCredit{}}, nil
|
||||
}
|
||||
|
||||
func (s *claudeResetHandlerStub) Redeem(_ context.Context, _ int64, key string) (*service.ClaudeResetOutcome, error) {
|
||||
s.calls++
|
||||
s.key = key
|
||||
if key == "" {
|
||||
return nil, service.ErrIdempotencyKeyRequired
|
||||
}
|
||||
return &service.ClaudeResetOutcome{Outcome: "reset", Cleared: []string{"five_hour"}}, nil
|
||||
}
|
||||
|
||||
func TestClaudeResetHandlerRedeemContract(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
stub := &claudeResetHandlerStub{}
|
||||
h := &AccountHandler{claudeResetCredits: stub}
|
||||
r := gin.New()
|
||||
r.POST("/accounts/:id/claude/reset-credits/redeem", h.RedeemClaudeResetCredit)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/accounts/1/claude/reset-credits/redeem", nil)
|
||||
req.Header.Set("Idempotency-Key", "same-operation")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, "same-operation", stub.key)
|
||||
require.Contains(t, w.Body.String(), `"outcome":"reset"`)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/accounts/1/claude/reset-credits/redeem", nil))
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
require.Contains(t, w.Body.String(), "IDEMPOTENCY_KEY_REQUIRED")
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/accounts/abc/claude/reset-credits/redeem", nil))
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
require.Equal(t, 2, stub.calls)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type moderationEngineSettings struct {
|
||||
service.SettingRepository
|
||||
value string
|
||||
}
|
||||
|
||||
func (r *moderationEngineSettings) GetValue(context.Context, string) (string, error) {
|
||||
return r.value, nil
|
||||
}
|
||||
func (r *moderationEngineSettings) Set(_ context.Context, _ string, value string) error {
|
||||
r.value = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestContentModerationEngineHandlerRoundTrip(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
settings := &moderationEngineSettings{value: `{"api_keys":["legacy-secret"],"model":"omni-moderation-latest"}`}
|
||||
svc := service.NewContentModerationService(settings, nil, nil, nil, nil, nil, nil, nil)
|
||||
h := NewContentModerationHandler(svc)
|
||||
router := gin.New()
|
||||
router.PUT("/config", h.UpdateConfig)
|
||||
router.GET("/config", h.GetConfig)
|
||||
body := `{"engine":"typesafe","engine_configs":{"typesafe":{"base_url":"https://api.typesafe.ai","model":"jev-latest","api_keys":["new-secret"],"thresholds":{"sexual":0.91}}}}`
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPut, "/config", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, 200, w.Code)
|
||||
require.Contains(t, w.Body.String(), `"engine":"typesafe"`)
|
||||
require.Contains(t, w.Body.String(), `"sexual":0.91`)
|
||||
require.NotContains(t, w.Body.String(), "new-secret")
|
||||
require.NotContains(t, w.Body.String(), "legacy-secret")
|
||||
w = httptest.NewRecorder()
|
||||
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/config", nil))
|
||||
require.Equal(t, 200, w.Code)
|
||||
require.Contains(t, w.Body.String(), `"engine_configs"`)
|
||||
require.Contains(t, w.Body.String(), `"model":"jev-latest"`)
|
||||
require.Contains(t, settings.value, "legacy-secret")
|
||||
require.Contains(t, settings.value, "new-secret")
|
||||
}
|
||||
@@ -20,10 +20,12 @@ func NewContentModerationHandler(svc *service.ContentModerationService) *Content
|
||||
}
|
||||
|
||||
type contentModerationConfigRequest struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Mode *string `json:"mode"`
|
||||
BaseURL *string `json:"base_url"`
|
||||
Model *string `json:"model"`
|
||||
Engine *string `json:"engine"`
|
||||
EngineConfigs map[string]service.UpdateContentModerationEngineInput `json:"engine_configs"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Mode *string `json:"mode"`
|
||||
BaseURL *string `json:"base_url"`
|
||||
Model *string `json:"model"`
|
||||
// 审计请求使用的代理服务器:null 不修改;0 清除(直连);>0 指定代理。
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
APIKey *string `json:"api_key"`
|
||||
@@ -58,13 +60,15 @@ type contentModerationConfigRequest struct {
|
||||
}
|
||||
|
||||
type contentModerationAPIKeyTestRequest struct {
|
||||
APIKeys []string `json:"api_keys"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Model string `json:"model"`
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Images []string `json:"images"`
|
||||
Engine string `json:"engine"`
|
||||
Thresholds *map[string]float64 `json:"thresholds"`
|
||||
APIKeys []string `json:"api_keys"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Model string `json:"model"`
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Images []string `json:"images"`
|
||||
}
|
||||
|
||||
type contentModerationHashRequest struct {
|
||||
@@ -87,6 +91,7 @@ func (h *ContentModerationHandler) UpdateConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
cfg, err := h.service.UpdateConfig(c.Request.Context(), service.UpdateContentModerationConfigInput{
|
||||
Engine: req.Engine, EngineConfigs: req.EngineConfigs,
|
||||
Enabled: req.Enabled,
|
||||
Mode: req.Mode,
|
||||
BaseURL: req.BaseURL,
|
||||
@@ -134,6 +139,7 @@ func (h *ContentModerationHandler) TestAPIKeys(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
result, err := h.service.TestAPIKeys(c.Request.Context(), service.TestContentModerationAPIKeysInput{
|
||||
Engine: req.Engine, Thresholds: req.Thresholds,
|
||||
APIKeys: req.APIKeys,
|
||||
BaseURL: req.BaseURL,
|
||||
Model: req.Model,
|
||||
|
||||
@@ -262,13 +262,18 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
upstreamModelMismatch, err := parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
|
||||
nativeCompactionV2, err := parseOptionalBoolDashboardFilter(c, "native_compaction_v2")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
|
||||
return
|
||||
}
|
||||
upstreamModelMismatch, err = parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
|
||||
return
|
||||
}
|
||||
|
||||
trend, hit, err := h.getUsageTrendCached(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID, accountID, groupID, model, requestType, stream, billingType, upstreamModelMismatch)
|
||||
trend, hit, err := h.getUsageTrendCached(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID, accountID, groupID, model, requestType, stream, nativeCompactionV2, billingType, upstreamModelMismatch)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "Failed to get usage trend")
|
||||
return
|
||||
@@ -349,13 +354,18 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
upstreamModelMismatch, err := parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
|
||||
nativeCompactionV2, err := parseOptionalBoolDashboardFilter(c, "native_compaction_v2")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
|
||||
return
|
||||
}
|
||||
upstreamModelMismatch, err = parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
|
||||
return
|
||||
}
|
||||
|
||||
stats, hit, err := h.getModelStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, modelSource, requestType, stream, billingType, upstreamModelMismatch)
|
||||
stats, hit, err := h.getModelStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, modelSource, requestType, stream, nativeCompactionV2, billingType, upstreamModelMismatch)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "Failed to get model statistics")
|
||||
return
|
||||
@@ -426,13 +436,18 @@ func (h *DashboardHandler) GetGroupStats(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
upstreamModelMismatch, err := parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
|
||||
nativeCompactionV2, err := parseOptionalBoolDashboardFilter(c, "native_compaction_v2")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
|
||||
return
|
||||
}
|
||||
upstreamModelMismatch, err = parseOptionalBoolDashboardFilter(c, "upstream_model_mismatch")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
|
||||
return
|
||||
}
|
||||
|
||||
stats, hit, err := h.getGroupStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, requestType, stream, billingType, upstreamModelMismatch)
|
||||
stats, hit, err := h.getGroupStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, requestType, stream, nativeCompactionV2, billingType, upstreamModelMismatch)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "Failed to get group statistics")
|
||||
return
|
||||
@@ -485,7 +500,11 @@ func (h *DashboardHandler) GetUserUsageTrend(c *gin.Context) {
|
||||
limit = 12
|
||||
}
|
||||
|
||||
trend, hit, err := h.getUserUsageTrendCached(c.Request.Context(), startTime, endTime, granularity, limit)
|
||||
metric := c.DefaultQuery("metric", "tokens")
|
||||
if metric != "tokens" && metric != "actual_cost" {
|
||||
metric = "tokens"
|
||||
}
|
||||
trend, hit, err := h.getUserUsageTrendCached(c.Request.Context(), startTime, endTime, granularity, limit, metric)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "Failed to get user usage trend")
|
||||
return
|
||||
@@ -701,6 +720,14 @@ func (h *DashboardHandler) GetUserBreakdown(c *gin.Context) {
|
||||
dim.Stream = &s
|
||||
}
|
||||
}
|
||||
if v := strings.TrimSpace(c.Query("native_compaction_v2")); v != "" {
|
||||
value, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid native_compaction_v2 value, use true or false")
|
||||
return
|
||||
}
|
||||
dim.NativeCompactionV2 = &value
|
||||
}
|
||||
if v := c.Query("billing_type"); v != "" {
|
||||
if bt, err := strconv.ParseInt(v, 10, 8); err == nil {
|
||||
btVal := int8(bt)
|
||||
|
||||
@@ -45,6 +45,7 @@ func (r *dashboardUsageRepoCacheProbe) GetUserUsageTrend(
|
||||
startTime, endTime time.Time,
|
||||
granularity string,
|
||||
limit int,
|
||||
metric string,
|
||||
) ([]usagestats.UserUsageTrendPoint, error) {
|
||||
r.usersTrendCalls.Add(1)
|
||||
return []usagestats.UserUsageTrendPoint{{
|
||||
@@ -115,4 +116,20 @@ func TestDashboardHandler_GetUserUsageTrend_UsesCache(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, rec2.Code)
|
||||
require.Equal(t, "hit", rec2.Header().Get("X-Snapshot-Cache"))
|
||||
require.Equal(t, int32(1), repo.usersTrendCalls.Load())
|
||||
|
||||
for _, tc := range []struct {
|
||||
metric, cache string
|
||||
calls int32
|
||||
}{
|
||||
{"actual_cost", "miss", 2},
|
||||
{"actual_cost", "hit", 2},
|
||||
{"tokens", "hit", 2},
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/dashboard/users-trend?start_date=2026-03-01&end_date=2026-03-07&granularity=day&limit=8&metric="+tc.metric, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, tc.cache, rec.Header().Get("X-Snapshot-Cache"))
|
||||
require.Equal(t, tc.calls, repo.usersTrendCalls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,19 @@ import (
|
||||
|
||||
type dashboardUsageRepoCapture struct {
|
||||
service.UsageLogRepository
|
||||
trendRequestType *int16
|
||||
trendStream *bool
|
||||
modelRequestType *int16
|
||||
modelStream *bool
|
||||
trendMismatch *bool
|
||||
modelMismatch *bool
|
||||
groupMismatch *bool
|
||||
rankingLimit int
|
||||
ranking []usagestats.UserSpendingRankingItem
|
||||
rankingTotal float64
|
||||
trendRequestType *int16
|
||||
trendStream *bool
|
||||
trendNativeCompaction *bool
|
||||
modelRequestType *int16
|
||||
modelStream *bool
|
||||
modelNativeCompaction *bool
|
||||
groupNativeCompaction *bool
|
||||
trendMismatch *bool
|
||||
modelMismatch *bool
|
||||
groupMismatch *bool
|
||||
rankingLimit int
|
||||
ranking []usagestats.UserSpendingRankingItem
|
||||
rankingTotal float64
|
||||
}
|
||||
|
||||
func (s *dashboardUsageRepoCapture) GetUsageTrendWithUsageFilters(
|
||||
@@ -35,6 +38,7 @@ func (s *dashboardUsageRepoCapture) GetUsageTrendWithUsageFilters(
|
||||
) ([]usagestats.TrendDataPoint, error) {
|
||||
s.trendRequestType = filters.RequestType
|
||||
s.trendStream = filters.Stream
|
||||
s.trendNativeCompaction = filters.NativeCompactionV2
|
||||
s.trendMismatch = filters.UpstreamModelMismatch
|
||||
return []usagestats.TrendDataPoint{}, nil
|
||||
}
|
||||
@@ -62,6 +66,7 @@ func (s *dashboardUsageRepoCapture) GetModelStatsWithUsageFiltersBySource(
|
||||
) ([]usagestats.ModelStat, error) {
|
||||
s.modelRequestType = filters.RequestType
|
||||
s.modelStream = filters.Stream
|
||||
s.modelNativeCompaction = filters.NativeCompactionV2
|
||||
s.modelMismatch = filters.UpstreamModelMismatch
|
||||
return []usagestats.ModelStat{}, nil
|
||||
}
|
||||
@@ -71,6 +76,7 @@ func (s *dashboardUsageRepoCapture) GetGroupStatsWithUsageFilters(
|
||||
startTime, endTime time.Time,
|
||||
filters usagestats.UsageLogFilters,
|
||||
) ([]usagestats.GroupStat, error) {
|
||||
s.groupNativeCompaction = filters.NativeCompactionV2
|
||||
s.groupMismatch = filters.UpstreamModelMismatch
|
||||
return []usagestats.GroupStat{}, nil
|
||||
}
|
||||
@@ -208,6 +214,48 @@ func TestDashboardModelStatsValidModelSource(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
func TestDashboardNativeCompactionFilterPropagatesAlongsideTransport(t *testing.T) {
|
||||
resetDashboardReadCachesForTest()
|
||||
repo := &dashboardUsageRepoCapture{}
|
||||
router := newDashboardRequestTypeTestRouter(repo)
|
||||
|
||||
for _, path := range []string{
|
||||
"/admin/dashboard/trend?request_type=stream&native_compaction_v2=true",
|
||||
"/admin/dashboard/models?request_type=stream&native_compaction_v2=true",
|
||||
"/admin/dashboard/groups?request_type=stream&native_compaction_v2=true",
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code, path)
|
||||
}
|
||||
|
||||
require.NotNil(t, repo.trendNativeCompaction)
|
||||
require.True(t, *repo.trendNativeCompaction)
|
||||
require.NotNil(t, repo.modelNativeCompaction)
|
||||
require.True(t, *repo.modelNativeCompaction)
|
||||
require.NotNil(t, repo.groupNativeCompaction)
|
||||
require.True(t, *repo.groupNativeCompaction)
|
||||
require.NotNil(t, repo.trendRequestType)
|
||||
require.Equal(t, int16(service.RequestTypeStream), *repo.trendRequestType)
|
||||
}
|
||||
|
||||
func TestDashboardNativeCompactionFilterRejectsInvalidBoolean(t *testing.T) {
|
||||
repo := &dashboardUsageRepoCapture{}
|
||||
router := newDashboardRequestTypeTestRouter(repo)
|
||||
|
||||
for _, path := range []string{
|
||||
"/admin/dashboard/trend?native_compaction_v2=invalid",
|
||||
"/admin/dashboard/models?native_compaction_v2=invalid",
|
||||
"/admin/dashboard/groups?native_compaction_v2=invalid",
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardModelAuditFilterPropagatesToTrendModelAndGroupQueries(t *testing.T) {
|
||||
resetDashboardReadCachesForTest()
|
||||
repo := &dashboardUsageRepoCapture{}
|
||||
|
||||
@@ -28,6 +28,7 @@ type dashboardTrendCacheKey struct {
|
||||
Model string `json:"model"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
NativeCompactionV2 *bool `json:"native_compaction_v2"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
|
||||
}
|
||||
@@ -42,6 +43,7 @@ type dashboardModelGroupCacheKey struct {
|
||||
ModelSource string `json:"model_source,omitempty"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
NativeCompactionV2 *bool `json:"native_compaction_v2"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
|
||||
}
|
||||
@@ -51,6 +53,7 @@ type dashboardEntityTrendCacheKey struct {
|
||||
EndTime string `json:"end_time"`
|
||||
Granularity string `json:"granularity"`
|
||||
Limit int `json:"limit"`
|
||||
Metric string `json:"metric,omitempty"`
|
||||
}
|
||||
|
||||
func cacheStatusValue(hit bool) string {
|
||||
@@ -85,6 +88,7 @@ func (h *DashboardHandler) getUsageTrendCached(
|
||||
model string,
|
||||
requestType *int16,
|
||||
stream *bool,
|
||||
nativeCompactionV2 *bool,
|
||||
billingType *int8,
|
||||
upstreamModelMismatch *bool,
|
||||
) ([]usagestats.TrendDataPoint, bool, error) {
|
||||
@@ -99,13 +103,14 @@ func (h *DashboardHandler) getUsageTrendCached(
|
||||
Model: model,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
NativeCompactionV2: nativeCompactionV2,
|
||||
BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
entry, hit, err := dashboardTrendCache.GetOrLoad(key, func() (any, error) {
|
||||
return h.dashboardService.GetUsageTrendWithUsageFilters(ctx, startTime, endTime, granularity, usagestats.UsageLogFilters{
|
||||
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
|
||||
Model: model, RequestType: requestType, Stream: stream, BillingType: billingType,
|
||||
Model: model, RequestType: requestType, Stream: stream, NativeCompactionV2: nativeCompactionV2, BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
})
|
||||
@@ -123,6 +128,7 @@ func (h *DashboardHandler) getModelStatsCached(
|
||||
modelSource string,
|
||||
requestType *int16,
|
||||
stream *bool,
|
||||
nativeCompactionV2 *bool,
|
||||
billingType *int8,
|
||||
upstreamModelMismatch *bool,
|
||||
) ([]usagestats.ModelStat, bool, error) {
|
||||
@@ -136,13 +142,14 @@ func (h *DashboardHandler) getModelStatsCached(
|
||||
ModelSource: usagestats.NormalizeModelSource(modelSource),
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
NativeCompactionV2: nativeCompactionV2,
|
||||
BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
entry, hit, err := dashboardModelStatsCache.GetOrLoad(key, func() (any, error) {
|
||||
return h.dashboardService.GetModelStatsWithUsageFiltersBySource(ctx, startTime, endTime, usagestats.UsageLogFilters{
|
||||
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
|
||||
RequestType: requestType, Stream: stream, BillingType: billingType,
|
||||
RequestType: requestType, Stream: stream, NativeCompactionV2: nativeCompactionV2, BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
}, modelSource)
|
||||
})
|
||||
@@ -159,6 +166,7 @@ func (h *DashboardHandler) getGroupStatsCached(
|
||||
userID, apiKeyID, accountID, groupID int64,
|
||||
requestType *int16,
|
||||
stream *bool,
|
||||
nativeCompactionV2 *bool,
|
||||
billingType *int8,
|
||||
upstreamModelMismatch *bool,
|
||||
) ([]usagestats.GroupStat, bool, error) {
|
||||
@@ -171,13 +179,14 @@ func (h *DashboardHandler) getGroupStatsCached(
|
||||
GroupID: groupID,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
NativeCompactionV2: nativeCompactionV2,
|
||||
BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
entry, hit, err := dashboardGroupStatsCache.GetOrLoad(key, func() (any, error) {
|
||||
return h.dashboardService.GetGroupStatsWithUsageFilters(ctx, startTime, endTime, usagestats.UsageLogFilters{
|
||||
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
|
||||
RequestType: requestType, Stream: stream, BillingType: billingType,
|
||||
RequestType: requestType, Stream: stream, NativeCompactionV2: nativeCompactionV2, BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
})
|
||||
@@ -205,15 +214,16 @@ func (h *DashboardHandler) getAPIKeyUsageTrendCached(ctx context.Context, startT
|
||||
return trend, hit, err
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) getUserUsageTrendCached(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, bool, error) {
|
||||
func (h *DashboardHandler) getUserUsageTrendCached(ctx context.Context, startTime, endTime time.Time, granularity string, limit int, metric string) ([]usagestats.UserUsageTrendPoint, bool, error) {
|
||||
key := mustMarshalDashboardCacheKey(dashboardEntityTrendCacheKey{
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
Granularity: granularity,
|
||||
Limit: limit,
|
||||
Metric: metric,
|
||||
})
|
||||
entry, hit, err := dashboardUsersTrendCache.GetOrLoad(key, func() (any, error) {
|
||||
return h.dashboardService.GetUserUsageTrend(ctx, startTime, endTime, granularity, limit)
|
||||
return h.dashboardService.GetUserUsageTrend(ctx, startTime, endTime, granularity, limit, metric)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, hit, err
|
||||
|
||||
@@ -44,6 +44,7 @@ type dashboardSnapshotV2Filters struct {
|
||||
Model string
|
||||
RequestType *int16
|
||||
Stream *bool
|
||||
NativeCompactionV2 *bool
|
||||
BillingType *int8
|
||||
UpstreamModelMismatch *bool
|
||||
}
|
||||
@@ -59,6 +60,7 @@ type dashboardSnapshotV2CacheKey struct {
|
||||
Model string `json:"model"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
NativeCompactionV2 *bool `json:"native_compaction_v2"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
|
||||
IncludeStats bool `json:"include_stats"`
|
||||
@@ -105,6 +107,7 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
|
||||
Model: filters.Model,
|
||||
RequestType: filters.RequestType,
|
||||
Stream: filters.Stream,
|
||||
NativeCompactionV2: filters.NativeCompactionV2,
|
||||
BillingType: filters.BillingType,
|
||||
UpstreamModelMismatch: filters.UpstreamModelMismatch,
|
||||
IncludeStats: includeStats,
|
||||
@@ -186,6 +189,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
|
||||
filters.Model,
|
||||
filters.RequestType,
|
||||
filters.Stream,
|
||||
filters.NativeCompactionV2,
|
||||
filters.BillingType,
|
||||
filters.UpstreamModelMismatch,
|
||||
)
|
||||
@@ -207,6 +211,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
|
||||
usagestats.ModelSourceRequested,
|
||||
filters.RequestType,
|
||||
filters.Stream,
|
||||
filters.NativeCompactionV2,
|
||||
filters.BillingType,
|
||||
filters.UpstreamModelMismatch,
|
||||
)
|
||||
@@ -227,6 +232,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
|
||||
filters.GroupID,
|
||||
filters.RequestType,
|
||||
filters.Stream,
|
||||
filters.NativeCompactionV2,
|
||||
filters.BillingType,
|
||||
filters.UpstreamModelMismatch,
|
||||
)
|
||||
@@ -237,7 +243,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
|
||||
}
|
||||
|
||||
if includeUsersTrend {
|
||||
usersTrend, _, err := h.getUserUsageTrendCached(ctx, startTime, endTime, granularity, usersTrendLimit)
|
||||
usersTrend, _, err := h.getUserUsageTrendCached(ctx, startTime, endTime, granularity, usersTrendLimit, "tokens")
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get user usage trend")
|
||||
}
|
||||
@@ -296,6 +302,14 @@ func parseDashboardSnapshotV2Filters(c *gin.Context) (*dashboardSnapshotV2Filter
|
||||
filters.Stream = &streamVal
|
||||
}
|
||||
|
||||
if nativeCompactionV2Str := strings.TrimSpace(c.Query("native_compaction_v2")); nativeCompactionV2Str != "" {
|
||||
value, err := strconv.ParseBool(nativeCompactionV2Str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filters.NativeCompactionV2 = &value
|
||||
}
|
||||
|
||||
if billingTypeStr := strings.TrimSpace(c.Query("billing_type")); billingTypeStr != "" {
|
||||
v, err := strconv.ParseInt(billingTypeStr, 10, 8)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
@@ -194,6 +195,7 @@ func (h *GrokOAuthHandler) scheduleGrokImportProbe(account *service.Account) {
|
||||
// ProvideAccountHandler injects the Grok active prober for production while
|
||||
// keeping NewAccountHandler convenient for focused unit tests.
|
||||
func ProvideAccountHandler(
|
||||
cfg *config.Config,
|
||||
adminService service.AdminService,
|
||||
oauthService *service.OAuthService,
|
||||
openaiOAuthService *service.OpenAIOAuthService,
|
||||
@@ -227,5 +229,6 @@ func ProvideAccountHandler(
|
||||
tokenCacheInvalidator,
|
||||
)
|
||||
handler.grokImportProber = grokQuotaService
|
||||
handler.cfg = cfg
|
||||
return handler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
grokMediaEligibilityModeAuto = "auto"
|
||||
grokMediaEligibilityModeEnabled = "enabled"
|
||||
grokMediaEligibilityModeDisabled = "disabled"
|
||||
)
|
||||
|
||||
type grokMediaEligibilityResponse struct {
|
||||
AccountID int64 `json:"account_id"`
|
||||
Mode string `json:"mode"`
|
||||
Eligible bool `json:"eligible"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type updateGrokMediaEligibilityRequest struct {
|
||||
Mode string `json:"mode" binding:"required"`
|
||||
}
|
||||
|
||||
// GetGrokMediaEligibility returns the current manual override and evaluated
|
||||
// media-routing decision for a Grok OAuth account.
|
||||
// GET /api/v1/admin/accounts/:id/grok-media-eligibility
|
||||
func (h *AccountHandler) GetGrokMediaEligibility(c *gin.Context) {
|
||||
account, err := h.getGrokOAuthAccount(c)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, buildGrokMediaEligibilityResponse(account))
|
||||
}
|
||||
|
||||
// UpdateGrokMediaEligibility updates only the manual media eligibility
|
||||
// override. It deliberately uses UpdateAccountExtra so unrelated account
|
||||
// extra fields are preserved.
|
||||
// PUT /api/v1/admin/accounts/:id/grok-media-eligibility
|
||||
func (h *AccountHandler) UpdateGrokMediaEligibility(c *gin.Context) {
|
||||
account, err := h.getGrokOAuthAccount(c)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req updateGrokMediaEligibilityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value any
|
||||
switch req.Mode {
|
||||
case grokMediaEligibilityModeAuto:
|
||||
value = nil
|
||||
case grokMediaEligibilityModeEnabled:
|
||||
value = true
|
||||
case grokMediaEligibilityModeDisabled:
|
||||
value = false
|
||||
default:
|
||||
response.ErrorFrom(c, infraerrors.BadRequest(
|
||||
"GROK_MEDIA_ELIGIBILITY_INVALID_MODE",
|
||||
"mode must be one of auto, enabled, disabled",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]any{service.GrokMediaEligibleExtraKey: value}
|
||||
if err := service.ValidateGrokMediaEligibilityExtra(account.Platform, updates); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if err := h.adminService.UpdateAccountExtra(c.Request.Context(), account.ID, updates); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := h.adminService.GetAccount(c.Request.Context(), account.ID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if err := ensureGrokOAuthAccount(updated); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, buildGrokMediaEligibilityResponse(updated))
|
||||
}
|
||||
|
||||
func (h *AccountHandler) getGrokOAuthAccount(c *gin.Context) (*service.Account, error) {
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, infraerrors.BadRequest("INVALID_ACCOUNT_ID", "Invalid account ID")
|
||||
}
|
||||
account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ensureGrokOAuthAccount(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func ensureGrokOAuthAccount(account *service.Account) error {
|
||||
if account == nil {
|
||||
return infraerrors.NotFound("ACCOUNT_NOT_FOUND", "account not found")
|
||||
}
|
||||
if account.Platform != service.PlatformGrok || account.Type != service.AccountTypeOAuth {
|
||||
return infraerrors.BadRequest(
|
||||
"GROK_MEDIA_ELIGIBILITY_UNSUPPORTED_ACCOUNT",
|
||||
"media eligibility is only supported for Grok OAuth accounts",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildGrokMediaEligibilityResponse(account *service.Account) grokMediaEligibilityResponse {
|
||||
eligible, reason := account.GrokMediaGenerationEligibility()
|
||||
return grokMediaEligibilityResponse{
|
||||
AccountID: account.ID,
|
||||
Mode: grokMediaEligibilityModeFromExtra(account.Extra),
|
||||
Eligible: eligible,
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func grokMediaEligibilityModeFromExtra(extra map[string]any) string {
|
||||
if extra != nil {
|
||||
if enabled, ok := extra[service.GrokMediaEligibleExtraKey].(bool); ok {
|
||||
if enabled {
|
||||
return grokMediaEligibilityModeEnabled
|
||||
}
|
||||
return grokMediaEligibilityModeDisabled
|
||||
}
|
||||
}
|
||||
return grokMediaEligibilityModeAuto
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type grokMediaEligibilityAdminServiceStub struct {
|
||||
*stubAdminService
|
||||
account *service.Account
|
||||
}
|
||||
|
||||
func (s *grokMediaEligibilityAdminServiceStub) GetAccount(_ context.Context, _ int64) (*service.Account, error) {
|
||||
return s.account, nil
|
||||
}
|
||||
|
||||
func (s *grokMediaEligibilityAdminServiceStub) UpdateAccountExtra(_ context.Context, _ int64, updates map[string]any) error {
|
||||
if s.account.Extra == nil {
|
||||
s.account.Extra = map[string]any{}
|
||||
}
|
||||
for key, value := range updates {
|
||||
if value == nil {
|
||||
delete(s.account.Extra, key)
|
||||
} else {
|
||||
s.account.Extra[key] = value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newGrokMediaEligibilityRouter(account *service.Account) *gin.Engine {
|
||||
stub := &grokMediaEligibilityAdminServiceStub{
|
||||
stubAdminService: newStubAdminService(),
|
||||
account: account,
|
||||
}
|
||||
h := NewAccountHandler(stub, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
router := gin.New()
|
||||
router.GET("/accounts/:id/grok-media-eligibility", h.GetGrokMediaEligibility)
|
||||
router.PUT("/accounts/:id/grok-media-eligibility", h.UpdateGrokMediaEligibility)
|
||||
return router
|
||||
}
|
||||
|
||||
func TestGrokMediaEligibilityHandlerGetReturnsModeAndReason(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := newGrokMediaEligibilityRouter(&service.Account{
|
||||
ID: 12,
|
||||
Platform: service.PlatformGrok,
|
||||
Type: service.AccountTypeOAuth,
|
||||
Extra: map[string]any{
|
||||
"grok_billing_snapshot": map[string]any{"status_code": 200, "partial": true},
|
||||
"unrelated_setting": "preserve",
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/accounts/12/grok-media-eligibility", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var envelope struct {
|
||||
Data grokMediaEligibilityResponse `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope))
|
||||
require.Equal(t, int64(12), envelope.Data.AccountID)
|
||||
require.Equal(t, grokMediaEligibilityModeAuto, envelope.Data.Mode)
|
||||
require.Equal(t, "billing_inconclusive", envelope.Data.Reason)
|
||||
}
|
||||
|
||||
func TestGrokMediaEligibilityHandlerUpdateModesPreservesExtra(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
account := &service.Account{
|
||||
ID: 12,
|
||||
Platform: service.PlatformGrok,
|
||||
Type: service.AccountTypeOAuth,
|
||||
Extra: map[string]any{"unrelated_setting": "preserve"},
|
||||
}
|
||||
router := newGrokMediaEligibilityRouter(account)
|
||||
|
||||
tests := []struct {
|
||||
mode string
|
||||
wantMode string
|
||||
wantExtra any
|
||||
wantEligible bool
|
||||
wantReason string
|
||||
}{
|
||||
{mode: "enabled", wantMode: grokMediaEligibilityModeEnabled, wantExtra: true, wantEligible: true, wantReason: "override_enabled"},
|
||||
{mode: "disabled", wantMode: grokMediaEligibilityModeDisabled, wantExtra: false, wantEligible: false, wantReason: "override_disabled"},
|
||||
{mode: "auto", wantMode: grokMediaEligibilityModeAuto, wantExtra: nil, wantEligible: false, wantReason: "billing_unobserved"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.mode, func(t *testing.T) {
|
||||
body, err := json.Marshal(map[string]string{"mode": tt.mode})
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPut, "/accounts/12/grok-media-eligibility", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
var envelope struct {
|
||||
Data grokMediaEligibilityResponse `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope))
|
||||
require.Equal(t, tt.wantMode, envelope.Data.Mode)
|
||||
require.Equal(t, tt.wantEligible, envelope.Data.Eligible)
|
||||
require.Equal(t, tt.wantReason, envelope.Data.Reason)
|
||||
require.Equal(t, "preserve", account.Extra["unrelated_setting"])
|
||||
if tt.wantExtra == nil {
|
||||
_, exists := account.Extra[service.GrokMediaEligibleExtraKey]
|
||||
require.False(t, exists)
|
||||
} else {
|
||||
require.Equal(t, tt.wantExtra, account.Extra[service.GrokMediaEligibleExtraKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokMediaEligibilityHandlerRejectsUnsupportedAndInvalidModes(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
account *service.Account
|
||||
body string
|
||||
}{
|
||||
{name: "unsupported account", account: &service.Account{ID: 1, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth}, body: `{"mode":"enabled"}`},
|
||||
{name: "invalid mode", account: &service.Account{ID: 1, Platform: service.PlatformGrok, Type: service.AccountTypeOAuth}, body: `{"mode":"force"}`},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
router := newGrokMediaEligibilityRouter(tt.account)
|
||||
req := httptest.NewRequest(http.MethodPut, "/accounts/1/grok-media-eligibility", bytes.NewBufferString(tt.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user