Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3af5443b22 | ||
|
|
7ba3e1ac52 | ||
|
|
9fb260439f | ||
|
|
07931bbb18 | ||
|
|
2307aa5ca7 | ||
|
|
1563db3f82 | ||
|
|
e440ac48c7 | ||
|
|
03e8ab4134 | ||
|
|
c40edb4070 | ||
|
|
7bb9c0ed7d | ||
|
|
5f43696a9a | ||
|
|
847c0c4526 | ||
|
|
4a02d80543 | ||
|
|
bd17411d0e | ||
|
|
c4ae3550dd | ||
|
|
2f43e72bb9 | ||
|
|
b8651947c3 | ||
|
|
96b160d9a0 | ||
|
|
6f972145b7 | ||
|
|
695ebede70 | ||
|
|
269a409241 | ||
|
|
73aabc861c | ||
|
|
3b81776429 | ||
|
|
cbe258fd12 | ||
|
|
1591477a3e | ||
|
|
e457f0fa22 | ||
|
|
c0c3e1cb47 | ||
|
|
f06bf181d2 | ||
|
|
7075ae0d82 | ||
|
|
a177b88e52 | ||
|
|
cfecc8d113 | ||
|
|
c416467882 | ||
|
|
f19095f96d | ||
|
|
625f1693cb | ||
|
|
f25f399be0 | ||
|
|
748b84a15a | ||
|
|
fa42c3d706 | ||
|
|
fb01f5df2c | ||
|
|
8238956799 | ||
|
|
e00a8abdd5 | ||
|
|
a52665d079 | ||
|
|
ba5b861ec0 | ||
|
|
817fd1214c | ||
|
|
41f6e63799 | ||
|
|
b07d85c497 | ||
|
|
cc894ef578 | ||
|
|
83d4eb6a43 | ||
|
|
ecce0769c0 | ||
|
|
377d1230fc | ||
|
|
6466978d2f | ||
|
|
3b8a148bcf | ||
|
|
3e45d4e030 | ||
|
|
f82d32207f | ||
|
|
40aaf7b3ae | ||
|
|
391d69e086 | ||
|
|
684d9efb1f | ||
|
|
26ac0498f2 | ||
|
|
40ea3aebad | ||
|
|
3fd66a33be | ||
|
|
4a1da29509 | ||
|
|
243921dc0a | ||
|
|
616df479e8 | ||
|
|
19da0f240e | ||
|
|
75faedda95 | ||
|
|
4eadee1074 | ||
|
|
31d5b67baa | ||
|
|
913ec5d74b | ||
|
|
77e0409f7c | ||
|
|
6a1efda0cc | ||
|
|
7498d8fdc8 | ||
|
|
cd05772e91 | ||
|
|
7a09a2eaf7 | ||
|
|
d493ce0bb2 | ||
|
|
5dfad32b87 | ||
|
|
ee62dfbaf1 | ||
|
|
d45135d87d | ||
|
|
d29d7f8cbc | ||
|
|
fd6cd474d6 | ||
|
|
73f6a590bf | ||
|
|
ffc01f9c66 | ||
|
|
6244090c1c | ||
|
|
844b118785 | ||
|
|
7c64a48dc2 | ||
|
|
86470628df | ||
|
|
fa4587041c | ||
|
|
cb8dabc12f | ||
|
|
b30651a0ad | ||
|
|
e2263d2565 | ||
|
|
cef18b4ad3 | ||
|
|
30ae15268f | ||
|
|
10081a8127 | ||
|
|
9f2f2738fd | ||
|
|
67380eafd5 | ||
|
|
a53150a7cf | ||
|
|
2ddda67354 | ||
|
|
40c26f343a | ||
|
|
d9d2854d27 | ||
|
|
219368ec6d | ||
|
|
fb56bcbd07 | ||
|
|
beaeaaed06 | ||
|
|
ccfced36ae | ||
|
|
77d8516e87 | ||
|
|
afa21336a5 | ||
|
|
5fc9778468 | ||
|
|
3445485ebc | ||
|
|
e45490a360 | ||
|
|
68653fb2cc | ||
|
|
f646a1f974 | ||
|
|
fd24923f61 | ||
|
|
b2d5ce0395 | ||
|
|
f96035462f | ||
|
|
4eb7630ab5 | ||
|
|
6816c63883 | ||
|
|
9f74eb57f4 | ||
|
|
98d60931c1 | ||
|
|
c2572535d8 | ||
|
|
9d5171c5d1 | ||
|
|
f6aa9dc3c8 | ||
|
|
a749673de3 | ||
|
|
2e279c81d2 | ||
|
|
d5824f6a5b | ||
|
|
1429e8f714 | ||
|
|
b2b2adcf8d | ||
|
|
3548256745 | ||
|
|
16b15e870d | ||
|
|
7e9af4c100 | ||
|
|
7c53a842e4 | ||
|
|
4033387fd4 | ||
|
|
f98a056f75 | ||
|
|
1bff06ea50 | ||
|
|
2e68b10aad | ||
|
|
39aaf2fea3 | ||
|
|
d78e366db5 | ||
|
|
cca235365b | ||
|
|
c628b3eea7 | ||
|
|
f7bc1970ef | ||
|
|
0b1f79c83c | ||
|
|
2ab24a1e77 | ||
|
|
ab9cb69e7e | ||
|
|
5ae254f771 | ||
|
|
17c0ee3859 | ||
|
|
ad87ddee12 | ||
|
|
8db8791a73 | ||
|
|
e62ec2c42f | ||
|
|
6c3edc0956 | ||
|
|
726de30101 | ||
|
|
2074fe3ba8 | ||
|
|
66808413df | ||
|
|
25da02dddd | ||
|
|
3243983b72 | ||
|
|
f7145c7508 | ||
|
|
0e05c61d36 | ||
|
|
01a0083948 | ||
|
|
b27cd76a85 | ||
|
|
b0b2734b0b | ||
|
|
f75c4161f2 | ||
|
|
e85348be80 | ||
|
|
61c2f5ad28 | ||
|
|
611a7c8ed3 | ||
|
|
ad26172b83 | ||
|
|
39485f2e28 | ||
|
|
787f875dd6 | ||
|
|
ed4207a16f | ||
|
|
5ade094318 | ||
|
|
953028718d | ||
|
|
2ab41b92b6 | ||
|
|
0117452551 | ||
|
|
48615d5d1f | ||
|
|
ccb20ace88 | ||
|
|
2bc139ab52 | ||
|
|
c0e073a798 | ||
|
|
75f88be5f7 | ||
|
|
740f580801 | ||
|
|
d1c6456d08 | ||
|
|
e4f869e0c7 | ||
|
|
c374ff2951 | ||
|
|
acce29af27 | ||
|
|
752b3d8574 | ||
|
|
b3092145db | ||
|
|
fb94fd352b | ||
|
|
cb7fef14c0 | ||
|
|
06fc0055cd | ||
|
|
9ede0f7165 | ||
|
|
85051616f9 | ||
|
|
6b0ec50f24 | ||
|
|
1b5dc676a9 | ||
|
|
394b12afdc | ||
|
|
2d03e40fd6 | ||
|
|
1f2a87adb0 | ||
|
|
5b2089c5a3 | ||
|
|
cf3577a3c5 | ||
|
|
d4d2c746c5 | ||
|
|
d536795e93 | ||
|
|
26be82cc85 | ||
|
|
7dae055f2a | ||
|
|
5b2a386ed7 | ||
|
|
fce90ecf89 | ||
|
|
1ab3256783 | ||
|
|
ec5a345935 | ||
|
|
32a0d9ba2d | ||
|
|
fbc9ee626d | ||
|
|
32ecd5cc62 | ||
|
|
38a5f266e9 | ||
|
|
b7fe8ebaad | ||
|
|
d5484866fa | ||
|
|
23dc9377bc | ||
|
|
68666e1f86 | ||
|
|
b0cdea303a | ||
|
|
1b30a2d747 | ||
|
|
82cbe6aff7 | ||
|
|
e2d9ce0cad | ||
|
|
99a8b84701 | ||
|
|
63839f193a | ||
|
|
994fbfeddb | ||
|
|
b0464a9863 | ||
|
|
f917d19d3e | ||
|
|
ae62854abc | ||
|
|
89d28037f2 | ||
|
|
e8b53c9195 | ||
|
|
6a945b3cad | ||
|
|
fefd0d5145 | ||
|
|
e4896c41d2 | ||
|
|
499a8ee423 | ||
|
|
b94e484e23 | ||
|
|
aa673062e2 | ||
|
|
bd1ccd9736 | ||
|
|
7d9c958482 | ||
|
|
2a5ae2b2dc | ||
|
|
4d3b300a25 | ||
|
|
e943f817b1 | ||
|
|
b171bb0e4a | ||
|
|
58e147fba6 | ||
|
|
892787723f | ||
|
|
bfac49fef9 | ||
|
|
82f7dd14f7 | ||
|
|
e61595fb35 | ||
|
|
c6f4fbde49 | ||
|
|
b228b93e9c | ||
|
|
b410c39133 | ||
|
|
359fd12b2e | ||
|
|
214210e1b2 | ||
|
|
85cb732cdb | ||
|
|
ac6208de16 | ||
|
|
2c250bfd73 | ||
|
|
c9effc4560 | ||
|
|
e2dfb3b8cb | ||
|
|
c41ae19e52 | ||
|
|
1128df2592 | ||
|
|
3d21d61609 | ||
|
|
49504adc98 | ||
|
|
e0c48a19ed | ||
|
|
dd04503e1f | ||
|
|
5cbd0c96a2 | ||
|
|
03c3f3b6fc | ||
|
|
22df600d09 | ||
|
|
8f6f459835 | ||
|
|
58ccea4eaa | ||
|
|
c253bd2c72 | ||
|
|
26cb59df05 | ||
|
|
58ea46e894 | ||
|
|
685455985b | ||
|
|
1ed3b6aef6 | ||
|
|
f211a630c8 | ||
|
|
37732dcd34 | ||
|
|
a341239596 | ||
|
|
1ba92449c7 | ||
|
|
9617775f9a | ||
|
|
fd42d3722d | ||
|
|
1870b58c1d | ||
|
|
269fbcac0e | ||
|
|
7e579cb28d | ||
|
|
ebcae03afd | ||
|
|
a9514a68d2 | ||
|
|
938f1868ae | ||
|
|
4d19836189 | ||
|
|
c792b44bb2 | ||
|
|
7376f4a48c | ||
|
|
1ea4150bf0 | ||
|
|
ed2da82396 | ||
|
|
6259940ef2 | ||
|
|
baaf59d417 | ||
|
|
c0325d24f9 | ||
|
|
c76ff55997 | ||
|
|
5c12960285 | ||
|
|
f6d6613ec6 | ||
|
|
5dfac8bffa | ||
|
|
632b4f4faa | ||
|
|
3d9a0a71bd | ||
|
|
1a3ecd2b94 | ||
|
|
8532344745 | ||
|
|
cddb03c0f1 | ||
|
|
a7a321232f | ||
|
|
302a10b886 | ||
|
|
a20e1c00cc | ||
|
|
16e4f7ecc3 | ||
|
|
bb6c3b4f6a | ||
|
|
c51fd7d0b3 | ||
|
|
7ab6d3db66 | ||
|
|
41344c20ff | ||
|
|
6a6fd304f6 | ||
|
|
c44711ac98 | ||
|
|
615e6901ea | ||
|
|
ab0fcd1a0e | ||
|
|
8869775ed3 | ||
|
|
f66a42c40e | ||
|
|
7d633f5fc3 | ||
|
|
b2d1c3859a | ||
|
|
7e45634df9 | ||
|
|
5253bb72b0 | ||
|
|
6bf335965a | ||
|
|
c46d07ca07 | ||
|
|
ab28a2d10b | ||
|
|
6793d5ac85 | ||
|
|
9f24a55305 | ||
|
|
401dd43b4b | ||
|
|
c38c5beef8 | ||
|
|
10c8b70203 | ||
|
|
7cdca9e495 | ||
|
|
971544570d | ||
|
|
612436a5a7 | ||
|
|
3c3bb2fa19 | ||
|
|
e330c243a8 | ||
|
|
8d82bb069c | ||
|
|
b8642ef674 | ||
|
|
76b70b1685 | ||
|
|
8a0a76aeb5 | ||
|
|
cb5e03a720 | ||
|
|
9aac3b73f7 | ||
|
|
e728545382 | ||
|
|
4b667ccd45 | ||
|
|
1977810cf2 | ||
|
|
79c2eb5020 | ||
|
|
5f19433103 | ||
|
|
396a9d1130 | ||
|
|
51016bb03a | ||
|
|
22fc0cdbf5 | ||
|
|
cb7841d85c | ||
|
|
e8ff2017c0 | ||
|
|
11e1e22882 | ||
|
|
b1e60ba453 | ||
|
|
a600dd1c0a | ||
|
|
5390647989 | ||
|
|
baeac1f3de | ||
|
|
c3063e01a4 | ||
|
|
901a0439f1 | ||
|
|
073e92d171 | ||
|
|
d677d67dda | ||
|
|
4d9fedee20 | ||
|
|
fce41e318f | ||
|
|
8ae6d8f67e | ||
|
|
8219dcfc87 | ||
|
|
1d3b9665c8 | ||
|
|
98c7b0e88e | ||
|
|
44ef88f659 | ||
|
|
c204d33b09 | ||
|
|
1e1798d90d | ||
|
|
45dcce0e49 | ||
|
|
89d826be29 | ||
|
|
cb7b03795d | ||
|
|
a8b9ea22b7 | ||
|
|
9662cff2e7 | ||
|
|
bafd2e293f | ||
|
|
21c07e8351 | ||
|
|
e7a3c1202e | ||
|
|
674570ca17 | ||
|
|
a288bab73a | ||
|
|
fbfdcef818 | ||
|
|
0e82efe489 | ||
|
|
e803e3851c | ||
|
|
5912ae960c | ||
|
|
e77b4b260e | ||
|
|
e215c98c2c | ||
|
|
e29b93a1fb | ||
|
|
fd82dfd52d | ||
|
|
4853662f99 | ||
|
|
0fa577a19c | ||
|
|
b61e4bcc45 | ||
|
|
0ae151a238 | ||
|
|
b830bc14d6 | ||
|
|
8c4c3c09ce | ||
|
|
678eb22a40 | ||
|
|
c4d883b8da | ||
|
|
0de6d7e9ba | ||
|
|
363cc4994b | ||
|
|
f3d9491071 | ||
|
|
76a13a5a8d | ||
|
|
69648476d4 | ||
|
|
a04ce49016 | ||
|
|
bb9e74285e | ||
|
|
bba6a55e0f | ||
|
|
5935e674a8 | ||
|
|
ef4f99f292 | ||
|
|
93c32fa1a2 | ||
|
|
04f8cdb194 | ||
|
|
c0ab3a00ea | ||
|
|
814ecfba7c | ||
|
|
bd404c16f2 | ||
|
|
4ec9ceec4a | ||
|
|
46cbb7187b | ||
|
|
80acae16fa | ||
|
|
19c6007a22 | ||
|
|
0ed1a9f22a | ||
|
|
a29fce4a61 | ||
|
|
1225437099 | ||
|
|
5192abb6b5 | ||
|
|
40aae11888 | ||
|
|
177bf30867 | ||
|
|
d76c1af759 | ||
|
|
da283854f6 | ||
|
|
670b03f7e7 | ||
|
|
a163742fc9 | ||
|
|
04dc540b23 | ||
|
|
943f09d357 | ||
|
|
c91fbeb0c0 | ||
|
|
c3fc4331c1 | ||
|
|
e087f1b736 | ||
|
|
145b0ac35e | ||
|
|
5ea03c178d | ||
|
|
1afb8264e5 | ||
|
|
2321c0e771 | ||
|
|
0b35370a7a | ||
|
|
9c36b75a7d | ||
|
|
900194fab2 | ||
|
|
662444774f | ||
|
|
e24cb99b79 | ||
|
|
1e618dbc29 | ||
|
|
a3bbf35cbd | ||
|
|
caa1abb13a | ||
|
|
574dfad2dc | ||
|
|
bc0a6a7039 | ||
|
|
6876477371 | ||
|
|
b918874f81 | ||
|
|
20a2d12dde | ||
|
|
ca9e2b48ee | ||
|
|
e499a54a9d | ||
|
|
6564d376e5 | ||
|
|
c8d9af6ce1 | ||
|
|
2d9920ba7d | ||
|
|
0d7b6ae64c | ||
|
|
fd9ce53281 | ||
|
|
12abb54700 | ||
|
|
9261dd7734 | ||
|
|
0f73203e35 | ||
|
|
e2d9034d3d | ||
|
|
ba4bd0c0b4 | ||
|
|
61acf29125 | ||
|
|
3e1674a060 | ||
|
|
0b3fe95afd | ||
|
|
e5b325e481 | ||
|
|
33351c7bc7 | ||
|
|
b689e5b401 | ||
|
|
9096492b55 | ||
|
|
3d3aee2e72 | ||
|
|
4d4a0be1ad | ||
|
|
591d47fb9b | ||
|
|
5350b3d98e | ||
|
|
10a4c6e3ad | ||
|
|
f3c7a1a8c4 | ||
|
|
f19e3816d0 | ||
|
|
30d0405388 | ||
|
|
5deeb7ef15 | ||
|
|
895e8247af | ||
|
|
2d0976ac2c | ||
|
|
4667f9012f | ||
|
|
af6928a268 | ||
|
|
358e4a89a1 | ||
|
|
b5e83156d9 | ||
|
|
fddc806db2 | ||
|
|
99b31067f7 | ||
|
|
f5c108c836 | ||
|
|
bbc8b6e906 | ||
|
|
118c1e8e72 | ||
|
|
48eb3766d2 | ||
|
|
29009f0b2e | ||
|
|
563a72ca73 | ||
|
|
f2da30bcd9 | ||
|
|
7821c4005e | ||
|
|
c5bda8b8e4 | ||
|
|
a1073843ac | ||
|
|
7a113fb7a3 | ||
|
|
909cbb9b12 | ||
|
|
d92edc01be | ||
|
|
fb0475656c | ||
|
|
6e34fb09c9 | ||
|
|
5315896b30 | ||
|
|
a54a4b674b | ||
|
|
b6eb6c1efa | ||
|
|
4999231d61 | ||
|
|
cbc2a3dd46 | ||
|
|
cbf2be05a3 | ||
|
|
e91b494168 | ||
|
|
04d9eeaf07 | ||
|
|
7eb1310701 | ||
|
|
280c1c8623 | ||
|
|
9f31df3fa8 | ||
|
|
825f9c78d5 | ||
|
|
cec922d335 | ||
|
|
a4faa015a7 | ||
|
|
3c22aeeb3d | ||
|
|
1f58e25ab3 | ||
|
|
07b46e93e4 | ||
|
|
cc67b1aca1 | ||
|
|
8ad0a5ff52 | ||
|
|
b7112d596f | ||
|
|
b717ed9d0d | ||
|
|
f3bac4619e | ||
|
|
2e857b20b8 | ||
|
|
b7863650ec | ||
|
|
bdeb13021a | ||
|
|
e01ce90d47 | ||
|
|
12db0f906a | ||
|
|
8005335742 | ||
|
|
64d1ebe4a5 | ||
|
|
9f3ee38d4e | ||
|
|
77e39fe608 | ||
|
|
85fb776151 | ||
|
|
6345b048d1 | ||
|
|
e1d6600eb2 | ||
|
|
8399a30417 | ||
|
|
35faaa6d21 | ||
|
|
85b65284ec | ||
|
|
0d98176c59 | ||
|
|
165b072908 | ||
|
|
2526a04226 | ||
|
|
68faeac837 | ||
|
|
6d632eec45 | ||
|
|
d0767eab9d | ||
|
|
9763765ebf | ||
|
|
74fcdf3d42 | ||
|
|
68d8f122e4 | ||
|
|
155c494964 | ||
|
|
8991574873 | ||
|
|
8f7b0a314d | ||
|
|
64090de664 | ||
|
|
de904c433e | ||
|
|
14a27f1960 | ||
|
|
c33c3208e3 | ||
|
|
de349187d9 | ||
|
|
815035fcc9 | ||
|
|
915cc7e7bd | ||
|
|
db0bff82c7 | ||
|
|
e88fc52ce6 | ||
|
|
045b620d0d | ||
|
|
e79429b03f | ||
|
|
02fbcbe3ad | ||
|
|
b5d9fd21b0 | ||
|
|
146b8b6684 | ||
|
|
a071b27b41 | ||
|
|
379db19141 | ||
|
|
f3c94d2099 | ||
|
|
e01c917a97 | ||
|
|
b8c0c00091 | ||
|
|
bd9b12bbcb | ||
|
|
72a56f862c | ||
|
|
e687ca3e9d | ||
|
|
8f5657b91c | ||
|
|
3a415e6d04 | ||
|
|
3ae94df72b | ||
|
|
d7c9e7167b | ||
|
|
99b357083e | ||
|
|
245d069602 | ||
|
|
ccf7ba3ba7 | ||
|
|
7a81468282 | ||
|
|
93a04567d4 | ||
|
|
918b121878 | ||
|
|
be5e4226d4 | ||
|
|
5bb206ee99 | ||
|
|
856a96a217 | ||
|
|
7b93f4a689 | ||
|
|
61b975b5dd | ||
|
|
769f1c2def | ||
|
|
21d0905c78 | ||
|
|
962da308cc | ||
|
|
a3aae134ee | ||
|
|
e549931cf4 | ||
|
|
5199a610af | ||
|
|
b59a3df5b5 | ||
|
|
25d2b03e90 | ||
|
|
32e4de7942 | ||
|
|
9b54b46b06 | ||
|
|
8f55e0a7cd | ||
|
|
5ec11885d1 | ||
|
|
c8af48d769 | ||
|
|
dce0f6b4e3 | ||
|
|
22ef761c27 | ||
|
|
865002fd2c | ||
|
|
a061a758f4 | ||
|
|
2a256150f7 | ||
|
|
79df1647d4 | ||
|
|
99ad01f6de | ||
|
|
a397724f69 | ||
|
|
2e20ea4c87 | ||
|
|
7c62382d04 | ||
|
|
0316994c52 | ||
|
|
2c8836e40b | ||
|
|
eb2e73df3d | ||
|
|
ff4bb3bd2c | ||
|
|
d0930c4bdb | ||
|
|
2413441b5f | ||
|
|
cda6de44da | ||
|
|
ec9e733606 | ||
|
|
dfca6246eb | ||
|
|
e12e0dc1a6 | ||
|
|
9bc99e8b6e | ||
|
|
e9aaa325fa | ||
|
|
43391de925 | ||
|
|
8989d25ed5 | ||
|
|
91f5b00679 | ||
|
|
48fc076ce1 | ||
|
|
9b7ea3aabf | ||
|
|
be925d9a42 | ||
|
|
0a28c99aae | ||
|
|
451abc3aa5 | ||
|
|
ba58e74b33 | ||
|
|
eb6c9663e7 | ||
|
|
370bdcf695 | ||
|
|
74249b8fed | ||
|
|
4a9459d607 | ||
|
|
59b5ac5458 | ||
|
|
d2d259cb7c | ||
|
|
8ea68bd689 | ||
|
|
4245cb41f4 | ||
|
|
242f2b78c2 | ||
|
|
d2d91ff9a8 | ||
|
|
d8c5024cee | ||
|
|
a5beecb92a | ||
|
|
ead73264c7 | ||
|
|
a58048ac32 | ||
|
|
93367b6db4 | ||
|
|
b6e53c9320 | ||
|
|
dbb42881c0 | ||
|
|
e4b0e1b66b | ||
|
|
02e50cc22d | ||
|
|
ab326c96eb | ||
|
|
287a9f386b | ||
|
|
8e102b3a0f | ||
|
|
e93f6b9953 | ||
|
|
a19c9f8d8a | ||
|
|
a1936d42db | ||
|
|
fe2c265c91 | ||
|
|
c123caddd4 | ||
|
|
e08aee49ed | ||
|
|
c9e60d1f26 | ||
|
|
47c03c75d8 | ||
|
|
5b7a64334f | ||
|
|
df249ee5d5 | ||
|
|
82ccc187cd | ||
|
|
ce14983133 | ||
|
|
fc5a1b78d2 | ||
|
|
8aa425d22f | ||
|
|
ba92d70422 | ||
|
|
2f109e74ca | ||
|
|
5c52fa93d5 | ||
|
|
00b8596176 | ||
|
|
c5e046b7d7 | ||
|
|
aac53afe0e | ||
|
|
f0e7a9c7a2 | ||
|
|
26e0a89323 | ||
|
|
d431c57f2e | ||
|
|
635a27189f | ||
|
|
d3b5703b55 | ||
|
|
7d38e67120 | ||
|
|
8b3fe664dc | ||
|
|
a4d263f62f | ||
|
|
ae81dfd933 | ||
|
|
35cab3c814 | ||
|
|
770e35b474 | ||
|
|
846dd310a3 | ||
|
|
9b4575e434 | ||
|
|
1f4cfc44c1 | ||
|
|
e592c5f9e0 | ||
|
|
9fd7e76231 | ||
|
|
785b61d424 | ||
|
|
2d3e845205 | ||
|
|
80af44cad7 | ||
|
|
c899c8cf37 | ||
|
|
4c4ff36380 | ||
|
|
c418fd522f | ||
|
|
1e08c4c56f | ||
|
|
2eb24814fe | ||
|
|
e2652eb853 | ||
|
|
66ad405dd6 | ||
|
|
1b04e03cc4 | ||
|
|
2be047d1cd | ||
|
|
db725a775a | ||
|
|
825ca7b1fc | ||
|
|
54a2bcfd15 | ||
|
|
27e8f69a9e | ||
|
|
684ab20a0b | ||
|
|
0173830df6 | ||
|
|
724565e4aa | ||
|
|
a03de418c8 | ||
|
|
61ebdbdd43 | ||
|
|
a1f1a0cc6b | ||
|
|
954d44c19c | ||
|
|
38081ef72e | ||
|
|
3c20f9a666 | ||
|
|
e1b76e2245 | ||
|
|
f970bd48c9 | ||
|
|
a0802f00b6 | ||
|
|
0b9f40e230 | ||
|
|
0b26acac07 | ||
|
|
7e2e9ba050 | ||
|
|
2a42833c4c | ||
|
|
c043c24774 | ||
|
|
11c1e944b9 | ||
|
|
d99ee72911 | ||
|
|
30d2589ef0 | ||
|
|
dec47e8fae | ||
|
|
fad2f215e8 | ||
|
|
20ad5ec506 | ||
|
|
0b6b4ea956 | ||
|
|
b0f5007f04 | ||
|
|
56f3d3c9b0 | ||
|
|
d60f4e442b | ||
|
|
b74024c786 | ||
|
|
21aacde0b3 | ||
|
|
ddf4c6fd81 | ||
|
|
ff7ab2308d | ||
|
|
0204ce285e | ||
|
|
f3a3d86845 | ||
|
|
b22f73e725 | ||
|
|
d6d53052f8 | ||
|
|
bd52e5d770 | ||
|
|
d4cada3b6b | ||
|
|
8f5caef78e | ||
|
|
2ef1246295 | ||
|
|
dd9a177a62 | ||
|
|
85a27fae39 | ||
|
|
eb1c5c7ee8 | ||
|
|
d9fba8fe78 | ||
|
|
15b3c0c5aa | ||
|
|
2e338af822 | ||
|
|
682c4fe0e6 | ||
|
|
948b63c9ca | ||
|
|
4c80d160dd | ||
|
|
570ea74d12 | ||
|
|
2980ff3850 | ||
|
|
04c96a2015 | ||
|
|
07f980b99f | ||
|
|
fe21725865 | ||
|
|
698547418f | ||
|
|
f54e9827a0 | ||
|
|
d29acc29a5 | ||
|
|
66998918b6 | ||
|
|
da6194c1c3 | ||
|
|
132d446ca9 | ||
|
|
0eac363e67 | ||
|
|
796313e993 | ||
|
|
c772d18666 | ||
|
|
2bf9c6d56b | ||
|
|
94df1fffc2 | ||
|
|
30967d5d9a | ||
|
|
dfdbc27709 | ||
|
|
beeb2f989b | ||
|
|
77d4df9544 | ||
|
|
7ceabb3fd5 | ||
|
|
26d894ef4f | ||
|
|
d6467f6eb0 | ||
|
|
8ed9f754cf | ||
|
|
3deb2f17d8 | ||
|
|
baaae8e121 | ||
|
|
5c9629ddb7 | ||
|
|
4d13925c9e | ||
|
|
d74e669a23 | ||
|
|
7d3bf86e55 | ||
|
|
2a871ec852 | ||
|
|
a35ff9613e | ||
|
|
739c0ff9c5 | ||
|
|
3d99acb0ad | ||
|
|
7b6111f2f7 | ||
|
|
7045f89de4 | ||
|
|
b74cb7891c | ||
|
|
e3e033bb35 | ||
|
|
272735b0a7 | ||
|
|
0d5e3ca9b0 | ||
|
|
35e8ba2a3e | ||
|
|
a6d868f27b | ||
|
|
7d796f1119 | ||
|
|
3bff4b64b1 | ||
|
|
5e72deb7d4 |
@@ -49,6 +49,9 @@ coverage/
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# 本地闭源插件目录可能包含发布签名私钥,绝不能进入 Docker 构建上下文。
|
||||
/plugins/
|
||||
|
||||
# Local config
|
||||
config.yaml
|
||||
config.local.yaml
|
||||
|
||||
@@ -35,3 +35,10 @@ exceptions:
|
||||
mitigation: "Proxy configuration not user-controlled; upgrade when axios releases fix"
|
||||
expires_on: "2026-07-10"
|
||||
owner: "security@your-domain"
|
||||
- package: nanoid
|
||||
advisory: "GHSA-2v37-7h3g-55p8"
|
||||
severity: high
|
||||
reason: "Custom generator with size=0 not used; all nanoid calls use default size or positive values"
|
||||
mitigation: "No zero-size custom generators in codebase; upgrade when nanoid releases fix"
|
||||
expires_on: "2026-10-13"
|
||||
owner: "security@your-domain"
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
/bin/bash -n deploy/apple-container.sh
|
||||
/bin/bash deploy/tests/apple-container-test.sh
|
||||
/bin/sh deploy/tests/docker-compose-security-test.sh
|
||||
/bin/sh deploy/tests/docker-compose-gateway-env-test.sh
|
||||
/bin/sh deploy/tests/docker-runtime-resources-test.sh
|
||||
/bin/sh deploy/test-caddyfile-cache.sh
|
||||
|
||||
@@ -32,7 +33,7 @@ jobs:
|
||||
cache-dependency-path: backend/go.sum
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.26.5'
|
||||
go version | grep -q 'go1.27.0'
|
||||
- name: Unit tests
|
||||
working-directory: backend
|
||||
run: make test-unit
|
||||
@@ -72,10 +73,10 @@ jobs:
|
||||
cache-dependency-path: backend/go.sum
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.26.5'
|
||||
go version | grep -q 'go1.27.0'
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.9
|
||||
version: v2.13
|
||||
args: --timeout=30m
|
||||
working-directory: backend
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.26.5'
|
||||
go version | grep -q 'go1.27.0'
|
||||
|
||||
# Docker setup for GoReleaser
|
||||
- name: Set up QEMU
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
cache-dependency-path: backend/go.sum
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.26.5'
|
||||
go version | grep -q 'go1.27.0'
|
||||
- name: Run govulncheck
|
||||
working-directory: backend
|
||||
run: |
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
docs/claude-relay-service/
|
||||
# 本地杂项文档、测试数据和外部项目副本
|
||||
/docs-local/
|
||||
.codex
|
||||
|
||||
# ===================
|
||||
@@ -136,10 +137,23 @@ docs/*
|
||||
!docs/PAYMENT_CN.md
|
||||
!docs/ADMIN_PAYMENT_INTEGRATION_API.md
|
||||
!docs/ASYNC_IMAGE_TASKS.md
|
||||
!docs/BATCH_IMAGE_MVP.md
|
||||
!docs/COMPOSITE_GROUPS.md
|
||||
!docs/PLUGIN_DEVELOPMENT.md
|
||||
!docs/channel-monitor-v2-safe-defaults.md
|
||||
!docs/legal/
|
||||
!docs/legal/*.md
|
||||
!docs/screenshots/
|
||||
docs/screenshots/*
|
||||
!docs/screenshots/mobile-account-actions-menu.png
|
||||
.serena/
|
||||
.codex/
|
||||
frontend/coverage/
|
||||
aicodex
|
||||
output/
|
||||
.codegraph/
|
||||
|
||||
# Vitest / Vite cache at repo root
|
||||
.vite/
|
||||
# 本地闭源插件开发目录及构建产物不进入 Sub2API 仓库
|
||||
/plugins/
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
### 开发工具
|
||||
|
||||
```bash
|
||||
# golangci-lint v2.7
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7
|
||||
# golangci-lint(CI 用 v2.13,本地建议装同一版以免版本差异带来的噪音)
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13
|
||||
|
||||
# pnpm (前端包管理)
|
||||
npm install -g pnpm
|
||||
@@ -47,13 +47,13 @@ npm install -g pnpm
|
||||
|
||||
| Workflow | 触发条件 | 检查内容 |
|
||||
|----------|----------|----------|
|
||||
| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.7 |
|
||||
| **backend-ci.yml** | push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.13 |
|
||||
| **security-scan.yml** | push, pull_request, 每周一 | govulncheck + gosec + pnpm audit |
|
||||
| **release.yml** | tag `v*` | 构建发布(PR 不触发) |
|
||||
|
||||
### CI 要求
|
||||
|
||||
- Go 版本必须是 **1.25.7**
|
||||
- Go 版本必须是 **1.27.0**:三个 workflow 都用 `go-version-file: backend/go.mod` 取版本,随后硬断言 `go version | grep -q 'go1.27.0'`。升级 Go 时要同时改 `backend/go.mod`、`backend-ci.yml`(两处)、`release.yml`、`security-scan.yml` 里的这句断言,**以及三个 Dockerfile 里的 Go 构建镜像**(`Dockerfile` / `deploy/Dockerfile` 的 `ARG GOLANG_IMAGE`、`backend/Dockerfile` 的 `FROM golang:`)。前者漏了 CI 会在版本校验步骤直接失败;**后者漏了 CI 不会报,而是等到有人用这些 Dockerfile 构建时才失败**(`go.mod requires go >= X (running Y; GOTOOLCHAIN=local)`)。
|
||||
- 前端使用 `pnpm install --frozen-lockfile`,必须提交 `pnpm-lock.yaml`
|
||||
|
||||
### 本地测试命令
|
||||
@@ -203,7 +203,7 @@ go test -tags=integration ./...
|
||||
**解决**:
|
||||
```bash
|
||||
cd backend
|
||||
go generate ./ent # 重新生成 ent 代码
|
||||
go generate ./ent # 重新生成 ent 代码(json.RawMessage 字段会生成为同类型的 jsontext.Value,属预期)
|
||||
git add ent/ # 生成的文件也要提交
|
||||
```
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# =============================================================================
|
||||
|
||||
ARG NODE_IMAGE=node:24-alpine
|
||||
ARG GOLANG_IMAGE=golang:1.26.5-alpine
|
||||
ARG GOLANG_IMAGE=golang:1.27.0-alpine
|
||||
ARG ALPINE_IMAGE=alpine:3.21
|
||||
ARG POSTGRES_IMAGE=postgres:18-alpine
|
||||
ARG GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
.PHONY: build build-backend build-frontend test test-backend test-frontend test-frontend-critical
|
||||
|
||||
FRONTEND_CRITICAL_VITEST := \
|
||||
src/api/__tests__/client.spec.ts \
|
||||
src/api/__tests__/tokenRefresh.spec.ts \
|
||||
src/api/__tests__/channelMonitorV2.spec.ts \
|
||||
src/views/auth/__tests__/LinuxDoCallbackView.spec.ts \
|
||||
src/views/auth/__tests__/WechatCallbackView.spec.ts \
|
||||
src/views/user/__tests__/PaymentView.spec.ts \
|
||||
src/views/user/__tests__/PaymentResultView.spec.ts \
|
||||
src/views/user/__tests__/ChannelStatusView.mode.spec.ts \
|
||||
src/components/user/profile/__tests__/ProfileInfoCard.spec.ts \
|
||||
src/views/admin/__tests__/SettingsView.spec.ts
|
||||
src/views/admin/__tests__/SettingsView.spec.ts \
|
||||
src/features/channel-monitor-v2/__tests__/designSystem.structure.spec.ts \
|
||||
src/features/channel-monitor-v2/__tests__/monitorFormat.spec.ts \
|
||||
src/features/channel-monitor-v2/__tests__/monitorZoom.spec.ts
|
||||
|
||||
# 一键编译前后端
|
||||
build: build-backend build-frontend
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Sub2API
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
@@ -50,7 +50,7 @@ Please read the following carefully before using this project:
|
||||
|
||||
<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 a permanent 5% discount on all recharges.</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>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -58,11 +58,6 @@ 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://www.aicodemirror.com/register?invitecode=KMVZQM"><img src="assets/partners/logos/AICodeMirror.jpg" alt="AICodeMirror" width="150"></a></td>
|
||||
<td>Thanks to AICodeMirror for sponsoring this project! AICodeMirror provides official high-stability relay services for Claude Code / Codex / Gemini CLI, with enterprise-grade concurrency, fast invoicing, and 24/7 dedicated technical support. Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original price, with extra discounts on top-ups! AICodeMirror offers special benefits for sub2api users: register via <a href="https://www.aicodemirror.com/register?invitecode=KMVZQM">this link</a> to enjoy 20% off your first top-up, and enterprise customers can get up to 25% off!</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>
|
||||
@@ -84,12 +79,6 @@ Please read the following carefully before using this project:
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://unity2.ai/register?source=sub2api"><img src="assets/partners/logos/unity2.png" alt="unity2" width="150"></a></td>
|
||||
<td>Thanks to Unity2 for sponsoring this project! <a href="https://unity2.ai/register?source=sub2api">Unity2</a> is a high-performance AI model API relay for individuals, teams, and enterprises, handling 30B+ tokens/day with 5000 RPM concurrency. One API Key works across Claude Code, Codex, OpenAI models, IDE plugins, and Agent workflows, with balance billing, bundled subscriptions, enterprise invoicing, and 1-on-1 support. <a href="https://unity2.ai/register?source=sub2api">Register</a> to claim $2 in balance, plus $10 more by joining the official group — up to $12 in free credit.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://veilx.io/#/hello/SJRBRVDV"><img src="assets/partners/logos/veilx.png" alt="veilx" width="150"></a></td>
|
||||
<td>Thanks to Veilx for sponsoring this project! <a href="https://veilx.io/#/hello/SJRBRVDV">Veilx</a> CDN is purpose-built for large-scale AI API traffic, deeply optimized for relay services and call chains across OpenAI, Claude, Gemini, and scenarios like chat, image generation, embeddings, and streaming — delivering lower latency and higher stability under heavy concurrency. It also offers China three-network optimized return lines, making it ideal for global AI relay platforms, overseas AI SaaS, and cross-border high-concurrency deployments.
|
||||
@@ -102,32 +91,12 @@ Please read the following carefully before using this project:
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://666api.work/sub2api"><img src="assets/partners/logos/666api.jpg" alt="666api" width="150"></a></td>
|
||||
<td>Thanks to 666api for sponsoring this project! <a href="https://666api.work/sub2api">666api</a> is an all-in-one platform offering:<br>
|
||||
⚡ API Relay — Pay-as-you-go access to global models sourced 100% from official providers, up to 75% off official pricing<br>
|
||||
Exclusive: Zhipu GLM 50% off · DeepSeek V4-pro 50% off · Seedance 2.0 8% off (whitelisted) · HappyHorse Overseas 30% off (whitelisted)<br>
|
||||
🔑 GPT Subscription Accounts (same-origin IP included) · Global Residential IP <br>
|
||||
💰 Invoices supported
|
||||
</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://anpin.ai"><img src="assets/partners/logos/anpin.jpg" alt="anpin" width="150"></a></td>
|
||||
<td>Thanks to <a href="https://anpin.ai">anpin.ai</a> for sponsoring this project! anpin.ai is a premium AI relay service platform dedicated to advancing AI accessibility. With an advanced technical architecture and globally distributed deployment, it provides users with a direct high-speed channel to the world's top-tier large language models.<br>
|
||||
Self-built primary account pool: 1-3s ultra-fast response, supports channel-partner distribution<br>
|
||||
Extreme stability: multi-line intelligent routing + redundant backup system, ensuring year-round high-availability operation;<br>
|
||||
Model authenticity: no content intervention or secondary filtering — experience the purest, most powerful native model capabilities.<br>
|
||||
1:1 top-up, enterprise-grade service with invoicing available. Anpin AI is not just a relay — it's your secure, reliable, and efficient bridge to the frontier world of intelligence.
|
||||
</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.
|
||||
@@ -148,23 +117,51 @@ Model authenticity: no content intervention or secondary filtering — experienc
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/claudeapi.jpg" alt="claudeapi" width="150"></a></td>
|
||||
<td>Thanks to Claude API for sponsoring this project! <a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">Claude API</a> is an official-channel API provider focused on Claude models. Built on official Anthropic keys and the official AWS Bedrock channel, it delivers a stable integration experience for Claude Code and Agent applications, supports the full Claude model lineup, and retains official capabilities such as Tool Use and long context. The service involves no reverse engineering and no model degradation, making it a great fit for heavy Claude Code users, Agent engineers, and enterprise engineering teams. Register via the <a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">exclusive link</a> and contact customer support to receive free trial credits; invoicing and team onboarding are also supported.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/code0.jpg" alt="code0" width="150"></a></td>
|
||||
<td>Thanks to code0.ai for sponsoring this project! <a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">code0.ai</a> is an AI coding workbench for developers and engineering teams, aggregating mainstream agent coding capabilities such as Claude Code and Codex, and covering common development scenarios including code generation, project understanding, debugging and fixing, code review, and documentation generation. It suits independent developers, Agent engineers, open-source maintainers, and enterprise R&D teams, with invoicing and team onboarding supported. Register via the <a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">exclusive link</a> and contact customer support to receive free trial credits and experience a more efficient AI coding workflow.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://nagora.ai/"><img src="assets/partners/logos/nagora.png" alt="Nagora" width="150"></a></td>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://api.fenno.ai/s/dC4k"><img src="assets/partners/logos/fennoai.jpg" alt="FennoAI" width="150"></a></td>
|
||||
<td>Thanks to FennoAI for sponsoring this project! FennoAI is a high-stability, high-performance API relay provider for enterprise R&D teams and developers, compatible with the OpenAI and Anthropic protocols and seamlessly integrating with mainstream AI coding tools such as Codex, Claude Code, and OpenCode. The platform delivers enterprise-grade stability, supporting call volumes of 100 billion tokens per day, and supports business-to-business settlement and invoicing for both domestic and overseas entities to meet enterprise R&D and procurement needs. As an exclusive benefit for Sub2API users, purchase a subscription via the <a href="https://api.fenno.ai/s/dC4k">exclusive link</a> to get $50 worth of Coding Plan credit for only $1.99. Referral rewards are also available: invite friends to purchase and earn up to 20% commission — the more you invite, the more you earn.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://lanox.ai/?c=6"><img src="assets/partners/logos/lanox.jpg" alt="LanoX AI" width="150"></a></td>
|
||||
<td>Thank you to LanoX AI for sponsoring this project! <a href="https://lanox.ai/?c=6">LanoX AI</a> provides stable, cost-effective global model access services for developers, teams, and enterprises. 🎁 New User Benefits — Claim millions of free tokens, plus 500+ free models for easier low-cost testing, validation, and deployment 🧠 Global Leading Models — GPT · Claude · Gemini · Qwen · Grok... 🎬 Multimodal Creation — Seedance 2.0 · GPT Image · Gemini Nano Banana 🛡️ Enterprise-Grade Reliability — High availability 💎 native capability output 💎 no intelligence degradation 💎 no model mixing 💎 transparent usage and billing 💎 💰 Lower API Costs — Top-tier models from as low as 10% of official pricing, with clear documentation, simple integration, invoicing support, and enterprise-scale batch usage 🏢 Enterprise Choice — Ideal for AI products, Agents, content platforms, and R&D teams with high-volume model usage</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.rapidproxy.io/?ref=sub2api"><img src="assets/partners/logos/rapidproxy.jpg" alt="RapidProxy" width="150"></a></td>
|
||||
<td><a href="https://www.rapidproxy.io/?ref=sub2api">RapidProxy</a> is a data collection proxy solution built for developers, providing stable and reliable residential proxy services. With 90M+ global residential IPs and 200+ country coverage, intelligent rotation, and precise geo-targeting, it helps projects such as web scraping, AI data training, SEO monitoring, and e-commerce data analysis break through access restrictions and improve data collection efficiency. It supports mainstream automation frameworks such as Playwright, Selenium, and Puppeteer, with prices as low as $0.65/GB — <a href="https://www.rapidproxy.io/?ref=sub2api">start your free test now</a>.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://hao.ai"><img src="assets/partners/logos/haoai.png" alt="hao.ai" width="150"></a></td>
|
||||
<td><a href="https://hao.ai">hao.ai</a> is a high-speed, stable unified large-model API gateway for developers and teams. With a single API Key and a unified interface, you can access mainstream models such as GPT, Claude, and xAI Grok, with compatibility for common protocols and SDKs including OpenAI and Anthropic. The platform provides model routing, failover, team management, and complete request logs, with model prices as low as 15% of official reference pricing, helping users build AI applications more simply, more reliably, and at lower cost.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.swiftproxy.net/?ref=sub2api"><img src="assets/partners/logos/swiftprox.png" alt="Swiftproxy" width="150"></a></td>
|
||||
<td>Swiftproxy is a high-performance proxy solution built for developers, providing stable and reliable residential and static residential proxy services. With 90M+ clean residential IPs, global coverage, flexible rotation, and precise geo-targeting, it helps projects such as web scraping, AI automation, browser automation, SEO monitoring, and multi-account management overcome access restrictions and improve workflow efficiency. It supports HTTP(S) and SOCKS5 protocols, integrates with popular automation tools like Playwright, Selenium, and Puppeteer, with dynamic proxy traffic that never expires until used and free testing available — <a href="https://www.swiftproxy.net/?ref=sub2api">start your free test now</a>!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.duckip.cn/?keyword=cu7oog6y"><img src="assets/partners/logos/duckip.png" alt="DuckIP" width="150"></a></td>
|
||||
<td><a href="https://www.duckip.cn/?keyword=cu7oog6y">DuckIP</a> - 90M+ global residential network resources across 195+ countries and regions, with rotation and sticky sessions for public data collection, RAG updates, model evaluation, and multi-region data workloads. 🟢Residential Proxy - 20% Off; 🟢Static Residential Proxy - Starting at ¥50.00/IP; 🟢Unlimited Residential Proxy - Starting at ¥19.8/Hour. ✅Get 500M Free Trial.</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## Overview
|
||||
@@ -197,7 +194,7 @@ Community projects that extend or integrate with Sub2API:
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|------------|
|
||||
| Backend | Go 1.25.7, Gin, Ent |
|
||||
| Backend | Go 1.27.0, Gin, Ent |
|
||||
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||
| Database | PostgreSQL 15+ |
|
||||
| Cache/Queue | Redis 7+ |
|
||||
@@ -541,12 +538,6 @@ default:
|
||||
rate_multiplier: 1.0
|
||||
```
|
||||
|
||||
### Sora Status (Temporarily Unavailable)
|
||||
|
||||
> ⚠️ Sora-related features are temporarily unavailable due to technical issues in upstream integration and media delivery.
|
||||
> Please do not rely on Sora in production at this time.
|
||||
> Existing `gateway.sora_*` configuration keys are reserved and may not take effect until these issues are resolved.
|
||||
|
||||
Additional security-related options are available in `config.yaml`:
|
||||
|
||||
- `cors.allowed_origins` for CORS allowlist
|
||||
@@ -649,6 +640,32 @@ Or set `GATEWAY_OPENAI_WS_MODE_ROUTER_V2_ENABLED=true` in the environment.
|
||||
Use `http_bridge` for client-WebSocket/upstream-HTTP operation when rolling out
|
||||
or mitigating upstream WebSocket issues.
|
||||
|
||||
#### Force OpenAI upstream HTTP/SSE
|
||||
|
||||
When an egress proxy or network repeatedly reconnects OpenAI Responses
|
||||
WebSockets, set the global fallback in the persisted deployment configuration:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
openai_ws:
|
||||
force_http: true
|
||||
```
|
||||
|
||||
For Compose and Apple container deployments, the equivalent `.env` setting is:
|
||||
|
||||
```bash
|
||||
GATEWAY_OPENAI_WS_FORCE_HTTP=true
|
||||
```
|
||||
|
||||
This selects HTTP/SSE for OpenAI upstream Responses traffic that would
|
||||
otherwise use WebSocket. It does not change the client-facing protocol or force
|
||||
HTTP/1.1; configure `gateway.openai_http2.enabled` (or
|
||||
`GATEWAY_OPENAI_HTTP2_ENABLED=false`) separately when a proxy is incompatible
|
||||
with HTTP/2. Unlike the account-level `http_bridge` mode, this global fallback
|
||||
takes effect without enabling `mode_router_v2_enabled`. Keep the setting in the
|
||||
deployment's persisted `.env` or `config.yaml`, rather than inside a running
|
||||
container, so it is read again after an image update or container recreation.
|
||||
|
||||
#### ⚠️ Important: Creating the Admin Account
|
||||
|
||||
The initial admin account is **only created via the setup wizard** (served at `http://<host>:8080` on first run). The `default.admin_email` / `default.admin_password` fields in `config.yaml` are **not used** to create it — they exist in the template for historical reasons.
|
||||
@@ -727,7 +744,7 @@ Sub2API supports both Grok subscription accounts through xAI OAuth and standard
|
||||
- Codex CLI style Responses WebSocket ingress is accepted on the Responses targets and bridged to xAI HTTP/SSE Responses upstream
|
||||
- Text models: `grok-4.5`, `grok-4.3`, `grok-build-0.1`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`, and `grok-4.20-multi-agent-0309`
|
||||
- Media targets for Grok groups: `/v1/images/generations`, `/images/generations`, `/v1/images/edits`, `/images/edits`, `/v1/videos/generations`, `/videos/generations`, `/v1/videos/edits`, `/videos/edits`, `/v1/videos/extensions`, `/videos/extensions`, `/v1/videos/{request_id}`, and `/videos/{request_id}`. Generation, editing, and extension requests require the group image-generation permission.
|
||||
- Media models: `grok-imagine`, `grok-imagine-image-quality`, `grok-imagine-image`, `grok-imagine-edit`, `grok-imagine-video`, and `grok-imagine-video-1.5`
|
||||
- Media models: `grok-imagine`, `grok-imagine-image-quality`, `grok-imagine-image`, `grok-imagine-image-2.0`, `grok-imagine-edit`, `grok-imagine-video`, and `grok-imagine-video-1.5`
|
||||
- JSON image-edit and video-generation requests accept image references in `image`, `images`, `reference_images`, and `mask` objects. Use `url` for xAI-compatible payloads; the legacy `image_url` field remains accepted and is normalized to `url` before forwarding.
|
||||
- Out of scope for this provider: TTS, transcription, browser automation, cookies, and Grok web scraping
|
||||
|
||||
@@ -743,7 +760,7 @@ The Grok OAuth flow uses PKCE and does not require committing private secrets. T
|
||||
| `XAI_OAUTH_AUTHORIZE_URL` | `https://auth.x.ai/oauth2/authorize` |
|
||||
| `XAI_OAUTH_TOKEN_URL` | `https://auth.x.ai/oauth2/token` |
|
||||
| `XAI_BASE_URL` | `https://api.x.ai/v1`; runtime-diagnostics override (account `base_url` controls request forwarding) |
|
||||
| `XAI_GROK_CLI_VERSION` | `0.2.93`; optional override for the client identity sent to `cli-chat-proxy.grok.com` |
|
||||
| `XAI_GROK_CLI_VERSION` | `0.2.114`; optional override for the client identity sent to `cli-chat-proxy.grok.com`. The pinned value is also the floor: an override below it is dropped |
|
||||
|
||||
Administrators can create Grok OAuth or API-key accounts from the dashboard. OAuth authorization and reauthorization are also available through the admin API:
|
||||
|
||||
@@ -857,11 +874,11 @@ sub2api/
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date">
|
||||
<a href="https://star-history.dera.page/#Wei-Shaw/sub2api&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Sub2API
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<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>感谢 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>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -59,11 +59,6 @@
|
||||
<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://www.aicodemirror.com/register?invitecode=KMVZQM"><img src="assets/partners/logos/AICodeMirror.jpg" alt="AICodeMirror" width="150"></a></td>
|
||||
<td>感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定性中转服务,企业级并发、快速开票、7×24 小时专属技术支持。Claude Code / Codex / Gemini 官方通道低至原价 38% / 2% / 9%,充值更享额外折扣!AICodeMirror 为 sub2api 用户提供专属福利:通过<a href="https://www.aicodemirror.com/register?invitecode=KMVZQM">此链接</a>注册,首次充值立享 8 折优惠,企业客户最高可享 75 折!</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>
|
||||
@@ -87,12 +82,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://unity2.ai/register?source=sub2api"><img src="assets/partners/logos/unity2.png" alt="unity2" width="150"></a></td>
|
||||
<td>感谢 Unity2 赞助本项目! <a href="https://unity2.ai/register?source=sub2api">Unity2</a> 是面向个人开发者、团队、企业的高性能 AI 模型 API 中转平台,长期服务国内头部企业,日均承载超 300 亿 token 调用,支持 5000 RPM 级高并发。一个 API Key 即可适配 Claude Code、Codex、OpenAI 模型、IDE 插件和 Agent 工作流等场景。具备企业级稳定供应能力,在高并发、持续调用和团队集中采购场景下依然保持低延迟、高可用。同时支持余额计费、组合订阅、首充优惠、企业开票、专属 1v1 对接,适合个人高频使用和企业长期接入。现在注册 Unity2.ai 可领取 $2 余额,加入官方群再送 $10 余额,合计最高可领 $12 免费额度,适合先体验后长期使用。<a href="https://unity2.ai/register?source=sub2api">注册链接</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://veilx.io/#/hello/SJRBRVDV"><img src="assets/partners/logos/veilx.png" alt="veilx" width="150"></a></td>
|
||||
<td>感谢 Veilx 赞助本项目! <a href="https://veilx.io/#/hello/SJRBRVDV">Veilx</a> CDN 专为超大规模 API 请求场景打造,针对 AI 中转站业务与 AI API 调用链路进行了深度优化,轻松应对高并发、高频请求与大流量传输,为开发者与企业提供更快、更稳、更低延迟的加速体验。无论是 OpenAI、Claude、Gemini 等 AI 接口中转,还是聊天、绘图、Embedding、流式输出等复杂场景,Veilx 都能显著提升响应速度与连接稳定性,有效降低网络波动带来的超时与失败问题。同时,Veilx 提供中国三网优化回国极速线路,大幅提升中国大陆地区访问海外 AI 服务的速度与稳定性,特别适合全球 AI 中转平台、海外 AI SaaS、跨境业务与高并发 API 系统部署。专为 AI API 而生,让你的 AI 中转服务更快、更稳、更省心。<a href="https://veilx.io/#/hello/SJRBRVDV">购买地址</a>
|
||||
@@ -105,32 +94,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://666api.work/sub2api"><img src="assets/partners/logos/666api.jpg" alt="666api" width="150"></a></td>
|
||||
<td>感谢 666api 赞助本项目!<a href="https://666api.work/sub2api">666api</a> 是一站式综合服务平台,提供:<br>
|
||||
⚡ API 中转 — 全球模型按量计费接入,100% 官方源直供,最高 75 折优惠<br>
|
||||
独家特惠:智谱 GLM 5 折 · DeepSeek V4-pro 5 折 · Seedance 2.0 0.8 折(白名单)· HappyHorse 海外版 3 折(白名单)<br>
|
||||
🔑 GPT 订阅账号(含同源 IP)· 全球住宅 IP<br>
|
||||
💰 支持开票
|
||||
</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://anpin.ai"><img src="assets/partners/logos/anpin.jpg" alt="anpin" width="150"></a></td>
|
||||
<td>感谢 <a href="https://anpin.ai">anpin.ai</a> 赞助本项目!anpin.ai 是一家致力于推动 AI 普惠的高端 AI 中转服务平台。我们以先进的技术架构和全球分布式部署,为用户提供直达国际顶尖大模型的高速通道。<br>
|
||||
自建一手号池:1-3S超快响应 支持同行分发<br>
|
||||
极致稳定:多线智能路由 + 冗余备份系统,确保服务全年无休、高可用运行;<br>
|
||||
模型真实性:不做任何内容干预与二次过滤,让您体验到最纯粹、最强大的原生模型能力。<br>
|
||||
充值1:1 企业级服务可开票,安品Ai不只是中转站,更是您连接前沿智能世界的安全、可靠、高效桥梁
|
||||
</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>即可开始体验,轻松构建更稳定、高效的自动化工作流。
|
||||
@@ -151,23 +120,51 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/claudeapi.jpg" alt="claudeapi" width="150"></a></td>
|
||||
<td>感谢 Claude API 对本项目的赞助! <a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">Claude API</a> 是专注 Claude 模型的官方渠道 API 服务商,基于 Anthropic 官方 Key 与 AWS Bedrock 官方渠道,提供稳定的 Claude Code 与 Agent 应用接入体验,支持 Claude 全系列模型,保留 Tool Use、长上下文等官方能力。服务非逆向、非降智,适合 Claude Code 深度用户、Agent 工程师与企业技术团队使用。通过<a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">[专属链接]</a>注册后联系客服,可领取免费测试额度,并支持开票和团队对接。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/code0.jpg" alt="code0" width="150"></a></td>
|
||||
<td>感谢 code0.ai 对本项目的赞助! <a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">code0.ai</a> 是面向开发者与技术团队的 AI 编程工作台,聚合 Claude Code、Codex 等主流 Agent 编程能力,支持代码生成、项目理解、调试修复、代码审查与文档生成等常见研发场景。适合独立开发者、Agent 工程师、开源项目维护者和企业研发团队使用,支持开票和团队对接。通过<a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">[专属链接]</a>注册后联系客服,可领取免费测试额度,体验更高效的 AI 编程工作流。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://nagora.ai/"><img src="assets/partners/logos/nagora.png" alt="Nagora" width="150"></a></td>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://api.fenno.ai/s/dC4k"><img src="assets/partners/logos/fennoai.jpg" alt="FennoAI" width="150"></a></td>
|
||||
<td>感谢 FennoAI 赞助本项目!FennoAI 是一家面向企业研发团队和开发者的高稳定、高性能 API 中转服务商,兼容 OpenAI 与 Anthropic 协议,可无缝接入 Codex、Claude Code、OpenCode 等主流 AI 编程工具。平台具备企业级稳定性,可支撑千亿 Token/日的调用规模,并支持境内外主体公对公结算及开票,满足企业级研发与采购需求。作为 Sub2API 用户专属福利,通过<a href="https://api.fenno.ai/s/dC4k">专属链接</a>购买订阅,仅需 1.99 美元即可获得价值 50 美元的 Coding Plan 额度。同时支持邀请奖励,邀请好友购买最高可获得 20% 返佣,邀请越多,奖励越高。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://lanox.ai/?c=6"><img src="assets/partners/logos/lanox.jpg" alt="LanoX AI" width="150"></a></td>
|
||||
<td>感谢 LanoX AI 对本项目的赞助!<a href="https://lanox.ai/?c=6">LanoX AI</a> 为开发者、团队与企业提供稳定、高性价比的全球模型接入服务。 🎁 新用户福利 — 免费领取 百万 Token ,更有500+ 免费模型 — 低成本测试、验证、部署更轻松 🧠 全球主流模型 — GPT · Claude · Gemini · Qwen · Grok... 🎬 多模态创作 — Seedance 2.0 · GPT Image · Gemini Nano Banana 🛡️ 企业级稳定服务 — 高可用💎原生能力输出💎不降智💎不混模💎调用与计费透明💎 💰 更低调用成本 — 顶级模型低至官方价 1 折起,文档清晰、接入简单、支持开票与企业批量调用 🏢 企业优选 — 适用于 AI 产品、Agent、内容平台、研发团队批量调用</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.rapidproxy.io/?ref=sub2api"><img src="assets/partners/logos/rapidproxy.jpg" alt="RapidProxy" width="150"></a></td>
|
||||
<td><a href="https://www.rapidproxy.io/?ref=sub2api">RapidProxy</a> 是面向开发者的数据采集代理解决方案,提供稳定可靠的住宅代理服务。通过 9000 万+全球住宅 IP和 200+国家覆盖、智能轮换机制和精准地区定位能力,帮助爬虫、AI 数据训练、SEO 监控、电商数据分析等项目突破访问限制,提高数据采集效率。支持 Playwright、Selenium、Puppeteer 等主流自动化框架,价格低至 $0.65/GB,<a href="https://www.rapidproxy.io/?ref=sub2api">立即免费测试吧</a>。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://hao.ai"><img src="assets/partners/logos/haoai.png" alt="hao.ai" width="150"></a></td>
|
||||
<td><a href="https://hao.ai">hao.ai</a> 是面向开发者与团队的高速、稳定大模型统一 API 网关。通过一个 API Key 和统一接口,即可接入 GPT、Claude、xAI Grok 等主流模型,兼容 OpenAI、Anthropic 等常用协议与 SDK。平台提供模型路由、故障回退、团队管理及完整调用日志,模型价格低至官方参考价的 1.5 折,帮助用户更简单、更稳定、更低成本地构建 AI 应用。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.swiftproxy.net/?ref=sub2api"><img src="assets/partners/logos/swiftprox.png" alt="Swiftproxy" width="150"></a></td>
|
||||
<td>Swiftproxy 是面向开发者的高性能代理解决方案,提供稳定可靠的住宅代理和静态住宅代理服务。拥有 9000 万+ 纯净住宅 IP,覆盖全球,支持灵活轮换和精准地理定位,帮助网页抓取、AI 自动化、浏览器自动化、SEO 监控和多账号管理等项目突破访问限制,提升工作流效率。支持 HTTP(S) 和 SOCKS5 协议,兼容 Playwright、Selenium、Puppeteer 等主流自动化工具,动态代理流量用完为止永不过期,支持免费测试 — <a href="https://www.swiftproxy.net/?ref=sub2api">立即开始免费测试</a>!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.duckip.cn/?keyword=cu7oog6y"><img src="assets/partners/logos/duckip.png" alt="DuckIP" width="150"></a></td>
|
||||
<td><a href="https://www.duckip.cn/?keyword=cu7oog6y">DuckIP</a> - 9000 万+ 全球住宅网络资源,覆盖 195+ 国家和地区,支持轮换和粘性会话,适用于公共数据采集、RAG 更新、模型评估和多区域数据工作负载。🟢住宅代理 - 8 折优惠;🟢静态住宅代理 - ¥50.00/IP 起;🟢无限住宅代理 - ¥19.8/小时 起。✅免费领取 500M 试用流量。</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## 项目概述
|
||||
@@ -199,7 +196,7 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的
|
||||
|
||||
| 组件 | 技术 |
|
||||
|------|------|
|
||||
| 后端 | Go 1.25.7, Gin, Ent |
|
||||
| 后端 | Go 1.27.0, Gin, Ent |
|
||||
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||
| 数据库 | PostgreSQL 15+ |
|
||||
| 缓存/队列 | Redis 7+ |
|
||||
@@ -554,33 +551,6 @@ default:
|
||||
rate_multiplier: 1.0
|
||||
```
|
||||
|
||||
### Sora 功能状态(暂不可用)
|
||||
|
||||
> ⚠️ 当前 Sora 相关功能因上游接入与媒体链路存在技术问题,暂时不可用。
|
||||
> 现阶段请勿在生产环境依赖 Sora 能力。
|
||||
> 文档中的 `gateway.sora_*` 配置仅作预留,待技术问题修复后再恢复可用。
|
||||
|
||||
### Sora 媒体签名 URL(功能恢复后可选)
|
||||
|
||||
当配置 `gateway.sora_media_signing_key` 且 `gateway.sora_media_signed_url_ttl_seconds > 0` 时,网关会将 Sora 输出的媒体地址改写为临时签名 URL(`/sora/media-signed/...`)。这样无需 API Key 即可在浏览器中直接访问,且具备过期控制与防篡改能力(签名包含 path + query)。
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
# /sora/media 是否强制要求 API Key(默认 false)
|
||||
sora_media_require_api_key: false
|
||||
# 媒体临时签名密钥(为空则禁用签名)
|
||||
sora_media_signing_key: "your-signing-key"
|
||||
# 临时签名 URL 有效期(秒)
|
||||
sora_media_signed_url_ttl_seconds: 900
|
||||
```
|
||||
|
||||
> 若未配置签名密钥,`/sora/media-signed` 将返回 503。
|
||||
> 如需更严格的访问控制,可将 `sora_media_require_api_key` 设为 true,仅允许携带 API Key 的 `/sora/media` 访问。
|
||||
|
||||
访问策略说明:
|
||||
- `/sora/media`:内部调用或客户端携带 API Key 才能下载
|
||||
- `/sora/media-signed`:外部可访问,但有签名 + 过期控制
|
||||
|
||||
`config.yaml` 还支持以下安全相关配置:
|
||||
|
||||
- `cors.allowed_origins` 配置 CORS 白名单
|
||||
@@ -788,11 +758,11 @@ sub2api/
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date">
|
||||
<a href="https://star-history.dera.page/#Wei-Shaw/sub2api&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Sub2API
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
@@ -58,11 +58,6 @@
|
||||
<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://www.aicodemirror.com/register?invitecode=KMVZQM"><img src="assets/partners/logos/AICodeMirror.jpg" alt="AICodeMirror" width="150"></a></td>
|
||||
<td>AICodeMirror のご支援に感謝します!AICodeMirror は Claude Code / Codex / Gemini CLI の公式高安定性リレーサービスを提供しており、エンタープライズグレードの同時実行、迅速な請求書発行、24時間年中無休の専属テクニカルサポートを備えています。Claude Code / Codex / Gemini の公式チャネルを定価の 38% / 2% / 9% で利用可能、チャージ時にはさらに追加割引!AICodeMirror は sub2api ユーザー向けに特別特典を提供中:<a href="https://www.aicodemirror.com/register?invitecode=KMVZQM">こちらのリンク</a>から登録すると、初回チャージが 20% オフ、法人のお客様は最大 25% オフ!</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>
|
||||
@@ -86,12 +81,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://unity2.ai/register?source=sub2api"><img src="assets/partners/logos/unity2.png" alt="unity2" width="150"></a></td>
|
||||
<td>Unity2 のご支援に感謝します!<a href="https://unity2.ai/register?source=sub2api">Unity2</a> は個人開発者、チーム、企業向けの高性能 AI モデル API 中継プラットフォームです。中国の大手企業に長期にわたりサービスを提供しており、1日あたり 300 億以上のトークン呼び出しを処理し、5000 RPM 級の高並列性をサポートします。1つの API キーで Claude Code、Codex、OpenAI モデル、IDE プラグイン、Agent ワークフローなど様々なシナリオに対応できます。エンタープライズグレードの安定供給能力を備え、高並列・継続的な呼び出し・チームの集中購入シーンでも低レイテンシと高可用性を維持します。残高課金、組み合わせサブスクリプション、初回チャージ特典、企業向け請求書発行、専属 1v1 サポートにも対応しており、個人の頻繁な利用にも企業の長期導入にも適しています。今 Unity2.ai に登録すると $2 の残高、公式グループに参加するとさらに $10 の残高がもらえ、合計最大 $12 の無料クレジットを獲得できます — 試用後に長期利用したい方に最適です。<a href="https://unity2.ai/register?source=sub2api">登録リンク</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://veilx.io/#/hello/SJRBRVDV"><img src="assets/partners/logos/veilx.png" alt="veilx" width="150"></a></td>
|
||||
<td>Veilx のご支援に感謝します!<a href="https://veilx.io/#/hello/SJRBRVDV">Veilx</a> CDN は超大規模 API リクエストシナリオ向けに設計されており、AI 中継サービスと AI API 呼び出しチェーンに対して深く最適化されています。高並列・高頻度リクエスト・大容量トラフィックに容易に対応し、開発者と企業により高速で安定した、低レイテンシの加速体験を提供します。OpenAI、Claude、Gemini などの AI インターフェース中継はもちろん、チャット、画像生成、Embedding、ストリーミング出力などの複雑なシナリオでも、Veilx は応答速度と接続安定性を大幅に向上させ、ネットワーク変動によるタイムアウトや失敗を効果的に削減します。さらに、Veilx は中国三大ネットワーク最適化の高速回線を提供しており、中国本土から海外 AI サービスへのアクセス速度と安定性を大幅に向上させます。グローバル AI 中継プラットフォーム、海外 AI SaaS、越境ビジネス、高並列 API システム展開に特に適しています。AI API のために生まれ、あなたの AI 中継サービスをより速く、より安定して、より安心に。<a href="https://veilx.io/#/hello/SJRBRVDV">購入リンク</a>
|
||||
@@ -104,28 +93,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://666api.work/sub2api"><img src="assets/partners/logos/666api.jpg" alt="666api" width="150"></a></td>
|
||||
<td>666api のご支援に感謝します!<a href="https://666api.work/sub2api">666api</a> はオールインワンプラットフォームで、以下を提供しています:⚡ API 中継 — グローバルモデルへの従量課金アクセス、100% 公式ソースから直接供給、公式価格の最大 75% オフ。独占特典:Zhipu GLM 50% オフ・DeepSeek V4-pro 50% オフ・Seedance2.0 8% オフ(ホワイトリスト)・HappyHorse Overseas 30% オフ(ホワイトリスト)🔑 GPT サブスクリプションアカウント — 同源 IP 込み・グローバル住宅 IP 💰 請求書発行対応
|
||||
</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://anpin.ai"><img src="assets/partners/logos/anpin.jpg" alt="anpin" width="150"></a></td>
|
||||
<td><a href="https://anpin.ai">anpin.ai</a> のご支援に感謝します!anpin.ai は AI の普及推進に取り組むハイエンド AI 中継サービスプラットフォームです。先進的な技術アーキテクチャとグローバル分散デプロイメントにより、国際トップクラスの大規模モデルへの高速直通ルートを提供します。<br>
|
||||
自社構築の一次アカウントプール:1-3秒の超高速応答、同業者への配布に対応<br>
|
||||
極めて高い安定性:マルチライン・インテリジェントルーティング + 冗長バックアップシステムにより、年中無休の高可用性運用を確保;<br>
|
||||
モデルの真正性:一切のコンテンツ介入や二次フィルタリングを行わず、最も純粋で強力なネイティブモデル能力を体験できます。<br>
|
||||
チャージ 1:1、エンタープライズグレードサービスは請求書発行可能。安品 AI は単なる中継所ではなく、最先端のインテリジェント世界へと繋がる安全・信頼性・高効率の橋渡し役です。
|
||||
</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>して、より安定した効率的な自動化ワークフローを簡単に構築しましょう。
|
||||
@@ -146,23 +119,51 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/claudeapi.jpg" alt="claudeapi" width="150"></a></td>
|
||||
<td>Claude API のご支援に感謝します!<a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">Claude API</a> は Claude モデルに特化した公式チャネルの API サービスプロバイダーで、Anthropic 公式キーと AWS Bedrock 公式チャネルをベースに、Claude Code や Agent アプリケーションへの安定した接続体験を提供します。Claude 全シリーズのモデルに対応し、Tool Use や長文コンテキストなどの公式機能もそのまま利用可能。リバースエンジニアリングやモデル劣化のないサービスで、Claude Code のヘビーユーザー、Agent エンジニア、企業の技術チームに最適です。<a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">専用リンク</a>から登録後カスタマーサポートへご連絡いただくと、無料お試しクレジットを受け取れます。請求書発行やチーム導入にも対応しています。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default"><img src="assets/partners/logos/code0.jpg" alt="code0" width="150"></a></td>
|
||||
<td>code0.ai のご支援に感謝します!<a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">code0.ai</a> は開発者と技術チーム向けの AI プログラミングワークベンチで、Claude Code や Codex などの主要な Agent コーディング能力を集約し、コード生成、プロジェクト理解、デバッグと修正、コードレビュー、ドキュメント生成といった一般的な開発シーンをサポートします。個人開発者、Agent エンジニア、OSS メンテナー、企業の開発チームに最適で、請求書発行やチーム導入にも対応。<a href="https://code0.ai/agent/register/LgpIgl9JHtVG53V1?utm_source=zcf&utm_medium=partner&utm_campaign=zcf_2026&utm_content=default">専用リンク</a>から登録後カスタマーサポートへご連絡いただくと、無料お試しクレジットを受け取り、より効率的な AI プログラミングワークフローを体験できます。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://nagora.ai/"><img src="assets/partners/logos/nagora.png" alt="Nagora" width="150"></a></td>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://api.fenno.ai/s/dC4k"><img src="assets/partners/logos/fennoai.jpg" alt="FennoAI" width="150"></a></td>
|
||||
<td>FennoAI のご支援に感謝します!FennoAI は、企業の研究開発チームと開発者向けの高安定・高性能 API 中継サービスプロバイダーです。OpenAI と Anthropic のプロトコルに対応し、Codex、Claude Code、OpenCode などの主要 AI コーディングツールにシームレスに接続できます。エンタープライズ級の安定性を備え、1 日あたり千億規模の Token 呼び出しに対応。国内外法人間の企業間決済と請求書発行もサポートし、企業の研究開発・調達ニーズに応えます。Sub2API ユーザー限定特典として、<a href="https://api.fenno.ai/s/dC4k">専用リンク</a>からサブスクリプションを購入すると、わずか 1.99 ドルで 50 ドル相当の Coding Plan クレジットを獲得できます。さらに招待報酬にも対応しており、友達の購入で最大 20% の還元を獲得可能。招待が多いほど、報酬も増えます。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://lanox.ai/?c=6"><img src="assets/partners/logos/lanox.jpg" alt="LanoX AI" width="150"></a></td>
|
||||
<td>本プロジェクトをご支援いただいている <a href="https://lanox.ai/?c=6">LanoX AI</a> に感謝します!LanoX AI は、開発者、チーム、企業向けに、安定性とコストパフォーマンスに優れたグローバルモデル接続サービスを提供しています。 🎁 新規ユーザー特典 — 数百万 Token を無料で獲得可能。さらに 500+ の無料モデルで、低コストなテスト、検証、デプロイをより簡単に 🧠 世界の主要モデル — GPT · Claude · Gemini · Qwen · Grok... 🎬 マルチモーダル制作 — Seedance 2.0 · GPT Image · Gemini Nano Banana 🛡️ エンタープライズ級の安定性 — 高可用性 💎 ネイティブ能力の出力 💎 性能劣化なし 💎 モデル混在なし 💎 利用量と課金が透明 💎 💰 より低い利用コスト — トップモデルを公式価格の 1 割から利用可能。明確なドキュメント、簡単な接続、請求書発行、企業向け一括利用に対応 🏢 企業に最適 — AI プロダクト、Agent、コンテンツプラットフォーム、大量利用する開発チームに最適</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.rapidproxy.io/?ref=sub2api"><img src="assets/partners/logos/rapidproxy.jpg" alt="RapidProxy" width="150"></a></td>
|
||||
<td><a href="https://www.rapidproxy.io/?ref=sub2api">RapidProxy</a> は開発者向けのデータ収集プロキシソリューションであり、安定して信頼できる住宅用プロキシサービスを提供します。9,000 万以上のグローバル住宅 IP と 200 以上の国・地域のカバー、インテリジェントなローテーション機構、精密な地域ターゲティング機能により、クローラー、AI データ学習、SEO モニタリング、EC データ分析などのプロジェクトがアクセス制限を突破し、データ収集の効率を高めます。Playwright、Selenium、Puppeteer などの主要な自動化フレームワークに対応し、料金は $0.65/GB から。<a href="https://www.rapidproxy.io/?ref=sub2api">今すぐ無料でお試しください</a>。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://hao.ai"><img src="assets/partners/logos/haoai.png" alt="hao.ai" width="150"></a></td>
|
||||
<td><a href="https://hao.ai">hao.ai</a> は、開発者とチーム向けの高速で安定した大規模モデル統合 API ゲートウェイです。1 つの API Key と統一されたインターフェースで、GPT、Claude、xAI Grok などの主要モデルに接続でき、OpenAI や Anthropic などの一般的なプロトコルと SDK に対応しています。プラットフォームはモデルルーティング、フェイルオーバー、チーム管理、完全な呼び出しログを提供し、モデル価格は公式参考価格の 1.5 割から。よりシンプルに、より安定して、より低コストに AI アプリケーションを構築できます。</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.swiftproxy.net/?ref=sub2api"><img src="assets/partners/logos/swiftprox.png" alt="Swiftproxy" width="150"></a></td>
|
||||
<td>Swiftproxy は開発者向けの高性能プロキシソリューションで、安定して信頼できるレジデンシャルおよび静的レジデンシャルプロキシサービスを提供します。9,000 万以上のクリーンな住宅 IP を保有し、グローバルカバレッジ、柔軟なローテーション、精密なジオターゲティングにより、Web スクレイピング、AI オートメーション、ブラウザ自動化、SEO モニタリング、マルチアカウント管理などのプロジェクトがアクセス制限を克服し、ワークフロー効率を向上させます。HTTP(S) および SOCKS5 プロトコルに対応し、Playwright、Selenium、Puppeteer などの主要な自動化ツールと統合可能。動的プロキシトラフィックは使い切るまで期限切れなし、無料テストも利用可能 — <a href="https://www.swiftproxy.net/?ref=sub2api">今すぐ無料テストを開始</a>!</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.duckip.cn/?keyword=cu7oog6y"><img src="assets/partners/logos/duckip.png" alt="DuckIP" width="150"></a></td>
|
||||
<td><a href="https://www.duckip.cn/?keyword=cu7oog6y">DuckIP</a> - 195 以上の国と地域にわたる 9,000 万以上のグローバルレジデンシャルネットワークリソース。ローテーションとスティッキーセッションに対応し、パブリックデータ収集、RAG 更新、モデル評価、マルチリージョンデータワークロードに最適。🟢レジデンシャルプロキシ - 20% オフ;🟢スタティックレジデンシャルプロキシ - ¥50.00/IP から;🟢無制限レジデンシャルプロキシ - ¥19.8/時間 から。✅500M 無料トライアルを取得。</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
## 概要
|
||||
@@ -194,7 +195,7 @@ Sub2API を拡張・統合するコミュニティプロジェクト:
|
||||
|
||||
| コンポーネント | 技術 |
|
||||
|-----------|------------|
|
||||
| バックエンド | Go 1.25.7, Gin, Ent |
|
||||
| バックエンド | Go 1.27.0, Gin, Ent |
|
||||
| フロントエンド | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||
| データベース | PostgreSQL 15+ |
|
||||
| キャッシュ/キュー | Redis 7+ |
|
||||
@@ -537,12 +538,6 @@ default:
|
||||
rate_multiplier: 1.0
|
||||
```
|
||||
|
||||
### Sora ステータス(一時的に利用不可)
|
||||
|
||||
> ⚠️ Sora 関連の機能は、上流統合およびメディア配信の技術的問題により一時的に利用できません。
|
||||
> 現時点では本番環境で Sora に依存しないでください。
|
||||
> 既存の `gateway.sora_*` 設定キーは予約されていますが、これらの問題が解決されるまで有効にならない場合があります。
|
||||
|
||||
`config.yaml` では追加のセキュリティ関連オプションも利用できます:
|
||||
|
||||
- `cors.allowed_origins` - CORS 許可リスト
|
||||
@@ -719,11 +714,11 @@ sub2api/
|
||||
|
||||
## スター履歴
|
||||
|
||||
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date">
|
||||
<a href="https://star-history.dera.page/#Wei-Shaw/sub2api&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=Wei-Shaw/sub2api&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 744 KiB |
@@ -121,6 +121,16 @@ linters:
|
||||
# Default: true — must be true, ent generates 130K+ lines of code
|
||||
generated-is-used: true
|
||||
|
||||
exclusions:
|
||||
rules:
|
||||
# G703/G704 污点分析在测试文件中只会命中 httptest mock 与测试者自设的
|
||||
# 环境变量路径,不构成攻击面;且该分析跨环境结果不稳定(本地/CI 报告的
|
||||
# 位置集合不同),逐点 nolint 无法收敛,故按路径豁免。生产代码不豁免,
|
||||
# 必须逐点 //nolint:gosec 并写明信任边界。
|
||||
- path: '_test\.go$'
|
||||
linters: [ gosec ]
|
||||
text: 'G70[34]'
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.26.5-alpine
|
||||
FROM golang:1.27.0-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// profit-preview 读取生产只读导出的 JSON(分组利润配置、账号倍率与探测状态、
|
||||
// 用户覆盖倍率、主力模型清单),复用线上 U/D/阈值判定做五平台离线预演。
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// go run ./cmd/profit-preview -input dump.json [-assume-enabled] [-json]
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
type inputGroup struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
SubscriptionType string `json:"subscription_type"`
|
||||
ProfitControlEnabled bool `json:"profit_control_enabled"`
|
||||
ProfitMinMargin float64 `json:"profit_min_margin"`
|
||||
ProfitSafetyBuffer float64 `json:"profit_safety_buffer"`
|
||||
PeakRateEnabled bool `json:"peak_rate_enabled"`
|
||||
PeakStart string `json:"peak_start"`
|
||||
PeakEnd string `json:"peak_end"`
|
||||
PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
|
||||
}
|
||||
|
||||
type inputAccount struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
RateMultiplier *float64 `json:"rate_multiplier"`
|
||||
Extra map[string]any `json:"extra"`
|
||||
ModelMapping map[string]string `json:"model_mapping"`
|
||||
}
|
||||
|
||||
type inputEntry struct {
|
||||
Group inputGroup `json:"group"`
|
||||
Accounts []inputAccount `json:"accounts"`
|
||||
UserOverrides map[string]*float64 `json:"user_overrides"`
|
||||
Models []string `json:"models"`
|
||||
}
|
||||
|
||||
type inputDoc struct {
|
||||
Groups []inputEntry `json:"groups"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
inputPath := flag.String("input", "", "生产只读导出 JSON 路径")
|
||||
assumeEnabled := flag.Bool("assume-enabled", false, "把当前关闭的支持平台分组按保存配置视为已启用")
|
||||
jsonOut := flag.Bool("json", false, "以 JSON 输出完整报告(默认输出可读表格)")
|
||||
flag.Parse()
|
||||
if *inputPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: profit-preview -input dump.json [-assume-enabled] [-json]")
|
||||
os.Exit(2)
|
||||
}
|
||||
raw, err := os.ReadFile(*inputPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "read input: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
inputs, err := parsePreviewInputs(raw, *assumeEnabled)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "parse input: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
evalAt := time.Now()
|
||||
reports := service.PreviewProfitAdmission(inputs, evalAt)
|
||||
if len(reports) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "input produced no preview reports")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *jsonOut {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(map[string]any{"evaluated_at": evalAt, "reports": reports}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "write output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("利润门预演 @ %s(U=账号倍率;探测状态仅告警)\n", evalAt.Format(time.RFC3339))
|
||||
for _, report := range reports {
|
||||
fmt.Printf("\n== 分组 %d %s [%s] ==\n", report.GroupID, report.GroupName, report.Platform)
|
||||
fmt.Printf(" 利润门生效=%v 假定启用=%v | 默认 D=%.4f 阈值=%.4f | 最低有效 D=%.4f 阈值=%.4f\n",
|
||||
report.EffectiveGate, report.AssumedEnabled,
|
||||
report.DefaultD, report.ThresholdDefault, report.MinEffectiveD, report.ThresholdMinD)
|
||||
counts := map[string]int{}
|
||||
for _, v := range report.Verdicts {
|
||||
counts[v.Class]++
|
||||
rate := "-"
|
||||
if v.AccountRate != nil {
|
||||
rate = fmt.Sprintf("%.4f", *v.AccountRate)
|
||||
}
|
||||
flags := make([]string, 0, 2)
|
||||
if v.RejectedUnderMinD {
|
||||
flags = append(flags, "最低有效D下拒绝")
|
||||
}
|
||||
if len(v.Warnings) > 0 {
|
||||
flags = append(flags, strings.Join(v.Warnings, ","))
|
||||
}
|
||||
suffix := ""
|
||||
if len(flags) > 0 {
|
||||
suffix = " [" + strings.Join(flags, "; ") + "]"
|
||||
}
|
||||
fmt.Printf(" 账号 %-4d %-24s 平台=%-12s U=%-8s 来源=%-19s %s%s\n",
|
||||
v.AccountID, v.Name, v.Platform, rate, v.RateSource, v.Class, suffix)
|
||||
}
|
||||
fmt.Printf(" 分类合计: 准入=%d 利润不足=%d 倍率非法=%d\n",
|
||||
counts[service.ProfitPreviewClassAdmitted],
|
||||
counts[service.ProfitPreviewClassRejectedThreshold],
|
||||
counts[service.ProfitPreviewClassRejectedInvalidRate])
|
||||
models := make([]string, 0, len(report.RemainingByModel))
|
||||
for model := range report.RemainingByModel {
|
||||
models = append(models, model)
|
||||
}
|
||||
sort.Strings(models)
|
||||
for _, model := range models {
|
||||
fmt.Printf(" 模型 %-20s 利润门准入账号: 默认D=%d 最低有效D=%d\n",
|
||||
model, report.RemainingByModel[model], report.RemainingByModelMinD[model])
|
||||
}
|
||||
for _, model := range modelsWithZeroRemaining(report) {
|
||||
fmt.Printf(" 警告: 模型 %s 启用后利润门准入账号为 0\n", model)
|
||||
}
|
||||
for _, model := range modelsWithZeroRemainingUnderMinD(report) {
|
||||
fmt.Printf(" 警告: 模型 %s 在最低有效D(存在低倍率用户覆盖)下利润门准入账号为 0\n", model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parsePreviewInputs(raw []byte, assumeEnabled bool) ([]service.ProfitPreviewGroupInput, error) {
|
||||
var doc inputDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(doc.Groups) == 0 {
|
||||
return nil, fmt.Errorf("input contains no groups; check the export query and target configuration")
|
||||
}
|
||||
|
||||
inputs := make([]service.ProfitPreviewGroupInput, 0, len(doc.Groups))
|
||||
for i, entry := range doc.Groups {
|
||||
if entry.Group.ID <= 0 || strings.TrimSpace(entry.Group.Platform) == "" {
|
||||
return nil, fmt.Errorf("invalid group at index %d: id and platform are required", i)
|
||||
}
|
||||
group := &service.Group{
|
||||
ID: entry.Group.ID,
|
||||
Name: entry.Group.Name,
|
||||
Platform: entry.Group.Platform,
|
||||
Status: service.StatusActive,
|
||||
Hydrated: true,
|
||||
RateMultiplier: entry.Group.RateMultiplier,
|
||||
SubscriptionType: entry.Group.SubscriptionType,
|
||||
ProfitControlEnabled: entry.Group.ProfitControlEnabled,
|
||||
ProfitMinMargin: entry.Group.ProfitMinMargin,
|
||||
ProfitSafetyBuffer: entry.Group.ProfitSafetyBuffer,
|
||||
PeakRateEnabled: entry.Group.PeakRateEnabled,
|
||||
PeakStart: entry.Group.PeakStart,
|
||||
PeakEnd: entry.Group.PeakEnd,
|
||||
PeakRateMultiplier: entry.Group.PeakRateMultiplier,
|
||||
}
|
||||
accounts := make([]*service.Account, 0, len(entry.Accounts))
|
||||
for _, a := range entry.Accounts {
|
||||
account := &service.Account{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
Platform: a.Platform,
|
||||
Type: a.Type,
|
||||
RateMultiplier: a.RateMultiplier,
|
||||
Extra: a.Extra,
|
||||
}
|
||||
if len(a.ModelMapping) > 0 {
|
||||
mapping := make(map[string]any, len(a.ModelMapping))
|
||||
for k, v := range a.ModelMapping {
|
||||
mapping[k] = v
|
||||
}
|
||||
account.Credentials = map[string]any{"model_mapping": mapping}
|
||||
}
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
overrides := make(map[int64]float64, len(entry.UserOverrides))
|
||||
for userID, rate := range entry.UserOverrides {
|
||||
if rate == nil {
|
||||
continue
|
||||
}
|
||||
var id int64
|
||||
if _, err := fmt.Sscan(userID, &id); err == nil && id > 0 {
|
||||
overrides[id] = *rate
|
||||
}
|
||||
}
|
||||
inputs = append(inputs, service.ProfitPreviewGroupInput{
|
||||
Group: group,
|
||||
Accounts: accounts,
|
||||
UserOverrides: overrides,
|
||||
Models: entry.Models,
|
||||
AssumeEnabled: assumeEnabled,
|
||||
})
|
||||
}
|
||||
return inputs, nil
|
||||
}
|
||||
|
||||
func modelsWithZeroRemaining(report service.ProfitPreviewGroupReport) []string {
|
||||
var out []string
|
||||
for model, count := range report.RemainingByModel {
|
||||
if count == 0 {
|
||||
out = append(out, model)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// modelsWithZeroRemainingUnderMinD 返回默认 D 下仍有准入账号、但在最低有效 D
|
||||
// 下会归零的模型。最低有效 D 来自分组内最低的用户级倍率覆盖:这些模型对那部分
|
||||
// 用户是全黑的,而只看默认 D 的告警完全看不出来。
|
||||
// 两档都为 0 的模型由 modelsWithZeroRemaining 报告,这里不重复。
|
||||
func modelsWithZeroRemainingUnderMinD(report service.ProfitPreviewGroupReport) []string {
|
||||
var out []string
|
||||
for model, count := range report.RemainingByModelMinD {
|
||||
if count == 0 && report.RemainingByModel[model] > 0 {
|
||||
out = append(out, model)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParsePreviewInputsIgnoresNullUserOverride(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"groups": [{
|
||||
"group": {
|
||||
"id": 50,
|
||||
"name": "preview",
|
||||
"platform": "openai",
|
||||
"rate_multiplier": 0.5,
|
||||
"subscription_type": "standard",
|
||||
"profit_control_enabled": false,
|
||||
"profit_min_margin": 0.1,
|
||||
"profit_safety_buffer": 0
|
||||
},
|
||||
"accounts": [{
|
||||
"id": 1,
|
||||
"name": "cheap",
|
||||
"platform": "openai",
|
||||
"type": "apikey",
|
||||
"rate_multiplier": 0.2
|
||||
}],
|
||||
"user_overrides": {"40": null, "41": 0.4},
|
||||
"models": ["gpt-test"]
|
||||
}]
|
||||
}`)
|
||||
|
||||
inputs, err := parsePreviewInputs(raw, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, inputs, 1)
|
||||
require.Equal(t, map[int64]float64{41: 0.4}, inputs[0].UserOverrides)
|
||||
require.True(t, inputs[0].AssumeEnabled)
|
||||
|
||||
report := service.PreviewProfitAdmission(inputs, time.Date(2026, 1, 15, 8, 30, 0, 0, time.UTC))[0]
|
||||
require.InDelta(t, 0.4, report.MinEffectiveD, 1e-12, "null 覆盖不能被解码成 0 倍率")
|
||||
require.InDelta(t, 0.36, report.ThresholdMinD, 1e-12)
|
||||
}
|
||||
|
||||
func TestParsePreviewInputsRejectsEmptyGroups(t *testing.T) {
|
||||
for _, raw := range [][]byte{
|
||||
[]byte(`{"groups":null}`),
|
||||
[]byte(`{"groups":[]}`),
|
||||
} {
|
||||
inputs, err := parsePreviewInputs(raw, false)
|
||||
require.ErrorContains(t, err, "input contains no groups")
|
||||
require.Nil(t, inputs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModelsWithZeroRemainingWarnings 钉死两档 D 的归零告警分工:
|
||||
// 默认 D 归零由 modelsWithZeroRemaining 报告;默认 D 仍有账号但最低有效 D
|
||||
// (分组内存在更低的用户级倍率覆盖)归零的模型必须单独告警——那些用户的该
|
||||
// 模型会全黑,只看默认 D 完全看不出来。两档都为 0 时不重复告警。
|
||||
func TestModelsWithZeroRemainingWarnings(t *testing.T) {
|
||||
report := service.ProfitPreviewGroupReport{
|
||||
RemainingByModel: map[string]int{
|
||||
"both-zero": 0,
|
||||
"min-d-zero": 2,
|
||||
"healthy": 3,
|
||||
"min-d-zero-alt": 1,
|
||||
},
|
||||
RemainingByModelMinD: map[string]int{
|
||||
"both-zero": 0,
|
||||
"min-d-zero": 0,
|
||||
"healthy": 3,
|
||||
"min-d-zero-alt": 0,
|
||||
},
|
||||
}
|
||||
|
||||
if got := modelsWithZeroRemaining(report); len(got) != 1 || got[0] != "both-zero" {
|
||||
t.Fatalf("默认D归零告警应只覆盖 both-zero,got %v", got)
|
||||
}
|
||||
|
||||
got := modelsWithZeroRemainingUnderMinD(report)
|
||||
want := []string{"min-d-zero", "min-d-zero-alt"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("最低有效D归零告警不符: got %v want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("最低有效D归零告警不符(应按模型名排序): got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
0.1.168
|
||||
0.1.180
|
||||
|
||||
@@ -153,6 +153,11 @@ func runMainServer() {
|
||||
log.Fatalf("Failed to initialize application: %v", err)
|
||||
}
|
||||
defer app.Cleanup()
|
||||
if app.PluginManager != nil {
|
||||
if err := app.PluginManager.Start(context.Background()); err != nil {
|
||||
log.Printf("Plugin manager started in degraded state: %v", err)
|
||||
}
|
||||
}
|
||||
if app.PromptAudit != nil {
|
||||
if err := app.PromptAudit.Start(context.Background()); err != nil {
|
||||
// Startup continues so unrelated APIs stay up. Fail-closed (unavailable)
|
||||
|
||||
@@ -25,9 +25,10 @@ import (
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Server *http.Server
|
||||
PromptAudit *securityaudit.PromptService
|
||||
Cleanup func()
|
||||
Server *http.Server
|
||||
PromptAudit *securityaudit.PromptService
|
||||
PluginManager *service.PluginManager
|
||||
Cleanup func()
|
||||
}
|
||||
|
||||
func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
@@ -51,12 +52,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
|
||||
// BuildInfo provider
|
||||
provideServiceBuildInfo,
|
||||
providePluginHostInfo,
|
||||
|
||||
// Cleanup function provider
|
||||
provideCleanup,
|
||||
|
||||
// Application struct
|
||||
wire.Struct(new(Application), "Server", "PromptAudit", "Cleanup"),
|
||||
wire.Struct(new(Application), "Server", "PromptAudit", "PluginManager", "Cleanup"),
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
@@ -72,6 +74,13 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func providePluginHostInfo(buildInfo handler.BuildInfo) service.PluginHostInfo {
|
||||
return service.PluginHostInfo{
|
||||
Version: buildInfo.Version,
|
||||
BuildType: buildInfo.BuildType,
|
||||
}
|
||||
}
|
||||
|
||||
func provideCleanup(
|
||||
entClient *ent.Client,
|
||||
rdb *redis.Client,
|
||||
@@ -88,6 +97,8 @@ func provideCleanup(
|
||||
schedulerSnapshot *service.SchedulerSnapshotService,
|
||||
tokenRefresh *service.TokenRefreshService,
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
cnProviderBalanceCheck *service.CNProviderBalanceCheckService,
|
||||
codexVersionSync *service.OpenAICodexVersionSyncService,
|
||||
proxyExpiry *service.ProxyExpiryService,
|
||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||
usageCleanup *service.UsageCleanupService,
|
||||
@@ -109,11 +120,14 @@ func provideCleanup(
|
||||
backupSvc *service.BackupService,
|
||||
paymentOrderExpiry *service.PaymentOrderExpiryService,
|
||||
channelMonitorRunner *service.ChannelMonitorRunner,
|
||||
channelMonitorV2Aggregator *service.ChannelMonitorV2Aggregator,
|
||||
quotaFlusher *service.UserPlatformQuotaUsageFlusher,
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService,
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService,
|
||||
auditLog *service.AuditLogService,
|
||||
openAIAutoReset *service.OpenAIQuotaAutoResetService,
|
||||
promptAudit *securityaudit.PromptService,
|
||||
pluginManager *service.PluginManager,
|
||||
) func() {
|
||||
return func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
@@ -126,6 +140,18 @@ func provideCleanup(
|
||||
|
||||
// 应用层清理步骤可并行执行,基础设施资源(Redis/Ent)最后按顺序关闭。
|
||||
parallelSteps := []cleanupStep{
|
||||
{"PluginManager", func() error {
|
||||
if pluginManager != nil {
|
||||
pluginManager.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpenAIQuotaAutoResetService", func() error {
|
||||
if openAIAutoReset != nil {
|
||||
openAIAutoReset.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpsIngressRejectAggregator", func() error {
|
||||
if opsIngressReject != nil {
|
||||
opsIngressReject.Stop()
|
||||
@@ -236,6 +262,16 @@ func provideCleanup(
|
||||
accountExpiry.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"CNProviderBalanceCheckService", func() error {
|
||||
if cnProviderBalanceCheck != nil {
|
||||
cnProviderBalanceCheck.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpenAICodexVersionSyncService", func() error {
|
||||
codexVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ProxyExpiryService", func() error {
|
||||
proxyExpiry.Stop()
|
||||
return nil
|
||||
@@ -314,6 +350,12 @@ func provideCleanup(
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"ChannelMonitorV2Aggregator", func() error {
|
||||
if channelMonitorV2Aggregator != nil {
|
||||
channelMonitorV2Aggregator.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"ChannelMonitorRunner", func() error {
|
||||
if channelMonitorRunner != nil {
|
||||
channelMonitorRunner.Stop()
|
||||
|
||||
@@ -46,10 +46,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
return nil, err
|
||||
}
|
||||
userRepository := repository.NewUserRepository(client, db)
|
||||
passkeyRepository := repository.NewPasskeyRepository(db)
|
||||
redeemCodeRepository := repository.NewRedeemCodeRepository(client)
|
||||
redisClient := repository.ProvideRedis(configConfig)
|
||||
passkeySessionStore := repository.NewPasskeySessionStore(redisClient)
|
||||
refreshTokenCache := repository.NewRefreshTokenCache(redisClient)
|
||||
settingRepository := repository.NewSettingRepository(client)
|
||||
groupRepository := repository.NewGroupRepository(client, db)
|
||||
@@ -59,6 +57,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
emailService := service.NewEmailService(settingRepository, emailCache)
|
||||
turnstileVerifier := repository.NewTurnstileVerifier()
|
||||
turnstileService := service.NewTurnstileService(settingService, turnstileVerifier)
|
||||
tencentCaptchaVerifier := repository.NewTencentCaptchaVerifier()
|
||||
tencentCaptchaService := service.NewTencentCaptchaService(settingService, tencentCaptchaVerifier)
|
||||
aliyunCaptchaVerifier := repository.NewAliyunCaptchaVerifier()
|
||||
aliyunCaptchaService := service.NewAliyunCaptchaService(settingService, aliyunCaptchaVerifier)
|
||||
emailQueueService := service.ProvideEmailQueueService(emailService)
|
||||
promoCodeRepository := repository.NewPromoCodeRepository(client)
|
||||
billingCache := repository.NewBillingCache(redisClient)
|
||||
@@ -80,11 +82,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
subscriptionService := service.NewSubscriptionService(groupRepository, userSubscriptionRepository, billingCacheService, client, configConfig)
|
||||
affiliateRepository := repository.NewAffiliateRepository(client, db)
|
||||
affiliateService := service.NewAffiliateService(affiliateRepository, settingService, apiKeyAuthCacheInvalidator, billingCacheService)
|
||||
authService := service.NewAuthService(client, userRepository, redeemCodeRepository, refreshTokenCache, configConfig, settingService, emailService, turnstileService, emailQueueService, promoService, subscriptionService, affiliateService, serviceUserPlatformQuotaRepository)
|
||||
passkeyService, err := service.NewPasskeyService(configConfig, passkeyRepository, passkeySessionStore, userRepository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authService := service.ProvideAuthService(client, userRepository, redeemCodeRepository, refreshTokenCache, configConfig, settingService, emailService, turnstileService, tencentCaptchaService, aliyunCaptchaService, emailQueueService, promoService, subscriptionService, affiliateService, serviceUserPlatformQuotaRepository)
|
||||
userService := service.NewUserService(userRepository, settingRepository, apiKeyAuthCacheInvalidator, billingCache)
|
||||
redeemCache := repository.NewRedeemCache(redisClient)
|
||||
redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client, apiKeyAuthCacheInvalidator, affiliateService)
|
||||
@@ -98,7 +96,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
userAttributeValueRepository := repository.NewUserAttributeValueRepository(client)
|
||||
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
||||
authHandler := handler.NewAuthHandler(configConfig, authService, userService, settingService, promoService, redeemService, totpService, userAttributeService)
|
||||
passkeyHandler := handler.NewPasskeyHandler(passkeyService, authService, settingService)
|
||||
userHandler := handler.NewUserHandler(userService, authService, emailService, emailCache, affiliateService, serviceUserPlatformQuotaRepository)
|
||||
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
|
||||
usageLogRepository := repository.NewUsageLogRepository(client, db)
|
||||
@@ -152,7 +149,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
openAIOAuthService := service.ProvideOpenAIOAuthService(proxyRepository, openAIOAuthClient, privacyClientFactory)
|
||||
openAITokenProvider := service.ProvideOpenAITokenProvider(accountRepository, geminiTokenCache, openAIOAuthService, oAuthRefreshAPI)
|
||||
grokOAuthClient := repository.NewGrokOAuthClient()
|
||||
grokOAuthService := service.NewGrokOAuthService(proxyRepository, grokOAuthClient)
|
||||
grokOAuthService := service.ProvideGrokOAuthService(proxyRepository, grokOAuthClient, configConfig, redisClient)
|
||||
grokTokenProvider := service.ProvideGrokTokenProvider(accountRepository, geminiTokenCache, grokOAuthService, oAuthRefreshAPI, tempUnschedCache)
|
||||
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider, grokTokenProvider, modelPricingResolver, channelService, balanceNotifyService, settingService, serviceUserPlatformQuotaRepository)
|
||||
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
||||
@@ -177,8 +174,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
announcementService := service.NewAnnouncementService(announcementRepository, announcementReadRepository, userRepository, userSubscriptionRepository)
|
||||
announcementHandler := handler.NewAnnouncementHandler(announcementService)
|
||||
channelMonitorRepository := repository.NewChannelMonitorRepository(client, db)
|
||||
channelMonitorService := service.ProvideChannelMonitorService(channelMonitorRepository, secretEncryptor)
|
||||
channelMonitorService := service.ProvideChannelMonitorService(channelMonitorRepository, secretEncryptor, settingService)
|
||||
channelMonitorUserHandler := handler.NewChannelMonitorUserHandler(channelMonitorService, settingService)
|
||||
channelMonitorV2Repository := repository.NewChannelMonitorV2Repository(db)
|
||||
channelMonitorV2Service := service.ProvideChannelMonitorV2Service(channelMonitorV2Repository, settingService)
|
||||
channelMonitorV2Handler := handler.NewChannelMonitorV2Handler(channelMonitorV2Service)
|
||||
dashboardAggregationRepository := repository.NewDashboardAggregationRepository(db)
|
||||
dashboardStatsCache := repository.NewDashboardCache(redisClient, configConfig)
|
||||
dashboardService := service.NewDashboardService(usageLogRepository, dashboardAggregationRepository, dashboardStatsCache, configConfig)
|
||||
@@ -189,18 +189,21 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
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)
|
||||
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)
|
||||
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService, serviceUserPlatformQuotaRepository, billingCache, totpService, userService, settingService)
|
||||
groupCapacityService := service.NewGroupCapacityService(accountRepository, groupRepository, concurrencyService, sessionLimitCache, rpmCache)
|
||||
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService)
|
||||
claudeUsageFetcher := repository.NewClaudeUsageFetcher(httpUpstream)
|
||||
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
|
||||
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository, configConfig)
|
||||
grokQuotaFetcher := service.NewGrokQuotaFetcher()
|
||||
grokQuotaService := service.ProvideGrokQuotaService(accountRepository, proxyRepository, grokTokenProvider, httpUpstream, configConfig, usageLogRepository)
|
||||
grokQuotaService := service.ProvideGrokQuotaService(accountRepository, proxyRepository, grokTokenProvider, httpUpstream, configConfig, usageLogRepository, settingService)
|
||||
openAIQuotaService := service.ProvideOpenAIQuotaService(accountRepository, proxyRepository, openAITokenProvider, privacyClientFactory, openAIGatewayService)
|
||||
usageCache := service.NewUsageCache()
|
||||
accountUsageService := service.ProvideAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, grokQuotaFetcher, grokQuotaService, openAIQuotaService, usageCache, identityCache, tlsFingerprintProfileService, openAIGatewayService)
|
||||
accountTestService := service.ProvideAccountTestService(accountRepository, geminiTokenProvider, claudeTokenProvider, grokTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService, openAIGatewayService)
|
||||
pluginRepository := repository.NewPluginRepository(db)
|
||||
pluginHostInfo := providePluginHostInfo(buildInfo)
|
||||
pluginManager := service.NewPluginManager(pluginRepository, secretEncryptor, configConfig, pluginHostInfo)
|
||||
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)
|
||||
adminAnnouncementHandler := admin.NewAnnouncementHandler(announcementService)
|
||||
@@ -208,16 +211,19 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
dataManagementHandler := admin.NewDataManagementHandler(dataManagementService)
|
||||
backupObjectStoreFactory := repository.NewS3BackupStoreFactory()
|
||||
dbDumper := repository.NewPgDumper(configConfig)
|
||||
backupService := service.ProvideBackupService(settingRepository, configConfig, secretEncryptor, backupObjectStoreFactory, dbDumper)
|
||||
backupService := service.ProvideBackupService(settingRepository, configConfig, secretEncryptor, backupObjectStoreFactory, dbDumper, leaderLockCache, db)
|
||||
imageStorageFactory := repository.ProvideImageStorageFactory()
|
||||
imageStorageSettingService := service.ProvideImageStorageSettingService(settingRepository, secretEncryptor, backupService, imageStorageFactory, configConfig)
|
||||
backupHandler := admin.NewBackupHandler(backupService, userService, imageStorageSettingService)
|
||||
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
||||
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService, openAIQuotaService)
|
||||
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService, openAIQuotaService, rateLimitService)
|
||||
geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService)
|
||||
antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService)
|
||||
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, compositeTokenCacheInvalidator, schedulerCache, configConfig, tempUnschedCache, privacyClientFactory, proxyRepository, oAuthRefreshAPI, openAIGatewayService)
|
||||
grokOAuthHandler := admin.NewGrokOAuthHandler(grokOAuthService, adminService, grokQuotaService, tokenRefreshService)
|
||||
cnProviderQuotaService := service.ProvideCNProviderQuotaService(accountRepository, proxyRepository, httpUpstream, configConfig)
|
||||
cnProviderBalanceService := service.ProvideCNProviderBalanceService(accountRepository, proxyRepository, httpUpstream, configConfig)
|
||||
cnProviderHandler := admin.NewCNProviderHandler(cnProviderQuotaService, cnProviderBalanceService)
|
||||
proxyHandler := admin.NewProxyHandler(adminService)
|
||||
adminRedeemHandler := admin.NewRedeemHandler(adminService, redeemService)
|
||||
promoHandler := admin.NewPromoHandler(promoService)
|
||||
@@ -229,7 +235,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
registry := payment.ProvideRegistry()
|
||||
defaultLoadBalancer := payment.ProvideDefaultLoadBalancer(client, encryptionKey)
|
||||
paymentService := service.ProvidePaymentService(client, registry, defaultLoadBalancer, redeemService, subscriptionService, paymentConfigService, userRepository, groupRepository, affiliateService, notificationEmailService)
|
||||
settingHandler := handler.ProvideAdminSettingHandler(settingService, emailService, turnstileService, opsService, paymentConfigService, paymentService, userAttributeService, notificationEmailService, totpService, userService)
|
||||
settingHandler := handler.ProvideAdminSettingHandler(settingService, emailService, turnstileService, aliyunCaptchaService, opsService, paymentConfigService, paymentService, userAttributeService, notificationEmailService, totpService, userService)
|
||||
opsHandler := admin.NewOpsHandler(opsService)
|
||||
updateCache := repository.NewUpdateCache(redisClient)
|
||||
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
||||
@@ -248,6 +254,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache)
|
||||
errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService)
|
||||
tlsFingerprintProfileHandler := admin.NewTLSFingerprintProfileHandler(tlsFingerprintProfileService)
|
||||
pluginHandler := admin.NewPluginHandler(pluginManager)
|
||||
adminAPIKeyHandler := admin.NewAdminAPIKeyHandler(adminService)
|
||||
scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db)
|
||||
scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db)
|
||||
@@ -260,7 +267,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
channelMonitorRequestTemplateHandler := admin.NewChannelMonitorRequestTemplateHandler(channelMonitorRequestTemplateService)
|
||||
contentModerationRepository := repository.NewContentModerationRepository(db)
|
||||
contentModerationHashCache := repository.NewContentModerationHashCache(redisClient)
|
||||
contentModerationService := service.NewContentModerationService(settingRepository, contentModerationRepository, contentModerationHashCache, groupRepository, userRepository, apiKeyAuthCacheInvalidator, emailService)
|
||||
contentModerationService := service.NewContentModerationService(settingRepository, contentModerationRepository, contentModerationHashCache, groupRepository, userRepository, proxyRepository, apiKeyAuthCacheInvalidator, emailService)
|
||||
contentModerationHandler := admin.NewContentModerationHandler(contentModerationService)
|
||||
configManager := securityaudit.NewConfigManager(db, settingRepository, redisClient, secretEncryptor, configConfig)
|
||||
postgreSQLRepository := securityaudit.NewPostgreSQLRepository(db)
|
||||
@@ -277,20 +284,28 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
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, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, contentModerationHandler, promptAdminHandler, paymentHandler, affiliateHandler, complianceHandler, auditLogHandler, upstreamBillingProbeService, ollamaCloudUsageService)
|
||||
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)
|
||||
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, 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)
|
||||
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo, notificationEmailService)
|
||||
totpHandler := handler.NewTotpHandler(totpService)
|
||||
passkeyRepository := repository.NewPasskeyRepository(db)
|
||||
passkeySessionStore := repository.NewPasskeySessionStore(redisClient)
|
||||
passkeyService, err := service.NewPasskeyService(configConfig, passkeyRepository, passkeySessionStore, userRepository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passkeyHandler := handler.NewPasskeyHandler(passkeyService, authService, settingService)
|
||||
handlerPaymentHandler := handler.NewPaymentHandler(paymentService, paymentConfigService)
|
||||
paymentWebhookHandler := handler.NewPaymentWebhookHandler(paymentService, registry)
|
||||
availableChannelHandler := handler.NewAvailableChannelHandler(channelService, apiKeyService, settingService)
|
||||
modelPlazaHandler := handler.NewModelPlazaHandler(channelService, apiKeyService, settingService)
|
||||
modelPlazaService := service.NewModelPlazaService(channelRepository, groupRepository, pricingService, billingService, modelPricingResolver)
|
||||
modelPlazaHandler := handler.NewModelPlazaHandler(modelPlazaService, apiKeyService, settingService)
|
||||
imageTaskStore := repository.NewImageTaskStore(redisClient)
|
||||
imageTaskService := service.ProvideImageTaskService(imageTaskStore, imageStorageSettingService)
|
||||
asyncImageHandler := handler.NewAsyncImageHandler(imageTaskService, openAIGatewayHandler)
|
||||
@@ -304,7 +319,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
batchImageHandler := handler.ProvideBatchImageHandler(batchImagePublicService, batchImageDownloadService, batchImageCleanupService, openAIGatewayHandler)
|
||||
idempotencyCoordinator := service.ProvideIdempotencyCoordinator(idempotencyRepository, configConfig)
|
||||
idempotencyCleanupService := service.ProvideIdempotencyCleanupService(idempotencyRepository, configConfig)
|
||||
handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, announcementHandler, channelMonitorUserHandler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler, totpHandler, passkeyHandler, handlerPaymentHandler, paymentWebhookHandler, availableChannelHandler, modelPlazaHandler, asyncImageHandler, batchImageHandler, idempotencyCoordinator, idempotencyCleanupService)
|
||||
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)
|
||||
jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService, settingService, auditLogService)
|
||||
optionalJWTAuthMiddleware := middleware.NewOptionalJWTAuthMiddleware(authService, userService, settingService, auditLogService)
|
||||
adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService, auditLogService)
|
||||
@@ -320,18 +336,23 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
|
||||
opsIngressRejectAggregator := service.ProvideOpsIngressRejectAggregator(opsRepository, opsService)
|
||||
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
||||
cnProviderBalanceCheckService := service.ProvideCNProviderBalanceCheckService(accountRepository, cnProviderBalanceService, cnProviderQuotaService, configConfig)
|
||||
openAICodexVersionSyncService := service.ProvideOpenAICodexVersionSyncService(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)
|
||||
scheduledTestRunnerService := service.ProvideScheduledTestRunnerService(scheduledTestPlanRepository, scheduledTestService, accountTestService, rateLimitService, configConfig)
|
||||
paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService, leaderLockCache, db)
|
||||
channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService, settingService)
|
||||
channelMonitorQuotaFetcher := service.NewChannelMonitorQuotaFetcher(accountUsageService, cnProviderQuotaService, cnProviderBalanceService, accountRepository, configConfig)
|
||||
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, proxyExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, batchImageCleanupService, batchImageWorkerRuntime, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, openAIGatewayService, scheduledTestRunnerService, backupService, paymentOrderExpiryService, channelMonitorRunner, userPlatformQuotaUsageFlusher, upstreamBillingProbeService, ollamaCloudUsageService, auditLogService, promptService)
|
||||
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)
|
||||
application := &Application{
|
||||
Server: httpServer,
|
||||
PromptAudit: promptService,
|
||||
Cleanup: v,
|
||||
Server: httpServer,
|
||||
PromptAudit: promptService,
|
||||
PluginManager: pluginManager,
|
||||
Cleanup: v,
|
||||
}
|
||||
return application, nil
|
||||
}
|
||||
@@ -339,9 +360,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
// wire.go:
|
||||
|
||||
type Application struct {
|
||||
Server *http.Server
|
||||
PromptAudit *securityaudit.PromptService
|
||||
Cleanup func()
|
||||
Server *http.Server
|
||||
PromptAudit *securityaudit.PromptService
|
||||
PluginManager *service.PluginManager
|
||||
Cleanup func()
|
||||
}
|
||||
|
||||
func providePrivacyClientFactory() service.PrivacyClientFactory {
|
||||
@@ -355,6 +377,13 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func providePluginHostInfo(buildInfo handler.BuildInfo) service.PluginHostInfo {
|
||||
return service.PluginHostInfo{
|
||||
Version: buildInfo.Version,
|
||||
BuildType: buildInfo.BuildType,
|
||||
}
|
||||
}
|
||||
|
||||
func provideCleanup(
|
||||
entClient *ent.Client,
|
||||
rdb *redis.Client,
|
||||
@@ -371,6 +400,8 @@ func provideCleanup(
|
||||
schedulerSnapshot *service.SchedulerSnapshotService,
|
||||
tokenRefresh *service.TokenRefreshService,
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
cnProviderBalanceCheck *service.CNProviderBalanceCheckService,
|
||||
codexVersionSync *service.OpenAICodexVersionSyncService,
|
||||
proxyExpiry *service.ProxyExpiryService,
|
||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||
usageCleanup *service.UsageCleanupService,
|
||||
@@ -392,11 +423,14 @@ func provideCleanup(
|
||||
backupSvc *service.BackupService,
|
||||
paymentOrderExpiry *service.PaymentOrderExpiryService,
|
||||
channelMonitorRunner *service.ChannelMonitorRunner,
|
||||
channelMonitorV2Aggregator *service.ChannelMonitorV2Aggregator,
|
||||
quotaFlusher *service.UserPlatformQuotaUsageFlusher,
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService,
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService,
|
||||
auditLog *service.AuditLogService,
|
||||
openAIAutoReset *service.OpenAIQuotaAutoResetService,
|
||||
promptAudit *securityaudit.PromptService,
|
||||
pluginManager *service.PluginManager,
|
||||
) func() {
|
||||
return func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
@@ -408,6 +442,18 @@ func provideCleanup(
|
||||
}
|
||||
|
||||
parallelSteps := []cleanupStep{
|
||||
{"PluginManager", func() error {
|
||||
if pluginManager != nil {
|
||||
pluginManager.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpenAIQuotaAutoResetService", func() error {
|
||||
if openAIAutoReset != nil {
|
||||
openAIAutoReset.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpsIngressRejectAggregator", func() error {
|
||||
if opsIngressReject != nil {
|
||||
opsIngressReject.Stop()
|
||||
@@ -518,6 +564,16 @@ func provideCleanup(
|
||||
accountExpiry.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"CNProviderBalanceCheckService", func() error {
|
||||
if cnProviderBalanceCheck != nil {
|
||||
cnProviderBalanceCheck.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OpenAICodexVersionSyncService", func() error {
|
||||
codexVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ProxyExpiryService", func() error {
|
||||
proxyExpiry.Stop()
|
||||
return nil
|
||||
@@ -596,6 +652,12 @@ func provideCleanup(
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"ChannelMonitorV2Aggregator", func() error {
|
||||
if channelMonitorV2Aggregator != nil {
|
||||
channelMonitorV2Aggregator.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"ChannelMonitorRunner", func() error {
|
||||
if channelMonitorRunner != nil {
|
||||
channelMonitorRunner.Stop()
|
||||
|
||||
@@ -40,6 +40,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
nil,
|
||||
)
|
||||
accountExpirySvc := service.NewAccountExpiryService(nil, time.Second)
|
||||
codexVersionSyncSvc := service.NewOpenAICodexVersionSyncService(nil, nil, nil, time.Second)
|
||||
proxyExpirySvc := service.NewProxyExpiryService(nil, time.Second)
|
||||
subscriptionExpirySvc := service.NewSubscriptionExpiryService(nil, time.Second)
|
||||
pricingSvc := service.NewPricingService(cfg, nil)
|
||||
@@ -65,6 +66,8 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
schedulerSnapshotSvc,
|
||||
tokenRefreshSvc,
|
||||
accountExpirySvc,
|
||||
nil, // cnProviderBalanceCheck
|
||||
codexVersionSyncSvc,
|
||||
proxyExpirySvc,
|
||||
subscriptionExpirySvc,
|
||||
&service.UsageCleanupService{},
|
||||
@@ -86,11 +89,14 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
nil, // backupSvc
|
||||
nil, // paymentOrderExpiry
|
||||
nil, // channelMonitorRunner
|
||||
nil, // channelMonitorV2Aggregator
|
||||
nil, // quotaFlusher
|
||||
nil, // upstreamBillingProbe
|
||||
nil, // ollamaCloudUsage
|
||||
nil, // auditLog
|
||||
nil, // openAIAutoReset
|
||||
nil, // promptAudit
|
||||
nil, // pluginManager
|
||||
)
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
|
||||
@@ -27,9 +27,13 @@ type ChannelMonitor struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
// Provider holds the value of the "provider" field.
|
||||
Provider channelmonitor.Provider `json:"provider,omitempty"`
|
||||
// probe = LLM probe (default); quota = account usage only; quota_probe = both
|
||||
CheckMode string `json:"check_mode,omitempty"`
|
||||
// AccountID holds the value of the "account_id" field.
|
||||
AccountID *int64 `json:"account_id,omitempty"`
|
||||
// OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions
|
||||
APIMode string `json:"api_mode,omitempty"`
|
||||
// Provider base origin, e.g. https://api.openai.com
|
||||
// Provider base origin, e.g. https://api.openai.com; empty for quota-only monitors
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
// AES-256-GCM encrypted API key
|
||||
APIKeyEncrypted string `json:"-"`
|
||||
@@ -114,9 +118,9 @@ func (*ChannelMonitor) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new([]byte)
|
||||
case channelmonitor.FieldEnabled:
|
||||
values[i] = new(sql.NullBool)
|
||||
case channelmonitor.FieldID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldJitterSeconds, channelmonitor.FieldCreatedBy, channelmonitor.FieldTemplateID:
|
||||
case channelmonitor.FieldID, channelmonitor.FieldAccountID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldJitterSeconds, channelmonitor.FieldCreatedBy, channelmonitor.FieldTemplateID:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldAPIMode, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode:
|
||||
case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldCheckMode, channelmonitor.FieldAPIMode, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode:
|
||||
values[i] = new(sql.NullString)
|
||||
case channelmonitor.FieldCreatedAt, channelmonitor.FieldUpdatedAt, channelmonitor.FieldLastCheckedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
@@ -165,6 +169,19 @@ func (_m *ChannelMonitor) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.Provider = channelmonitor.Provider(value.String)
|
||||
}
|
||||
case channelmonitor.FieldCheckMode:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field check_mode", values[i])
|
||||
} else if value.Valid {
|
||||
_m.CheckMode = value.String
|
||||
}
|
||||
case channelmonitor.FieldAccountID:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field account_id", values[i])
|
||||
} else if value.Valid {
|
||||
_m.AccountID = new(int64)
|
||||
*_m.AccountID = value.Int64
|
||||
}
|
||||
case channelmonitor.FieldAPIMode:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field api_mode", values[i])
|
||||
@@ -326,6 +343,14 @@ func (_m *ChannelMonitor) String() string {
|
||||
builder.WriteString("provider=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Provider))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("check_mode=")
|
||||
builder.WriteString(_m.CheckMode)
|
||||
builder.WriteString(", ")
|
||||
if v := _m.AccountID; v != nil {
|
||||
builder.WriteString("account_id=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("api_mode=")
|
||||
builder.WriteString(_m.APIMode)
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -23,6 +23,10 @@ const (
|
||||
FieldName = "name"
|
||||
// FieldProvider holds the string denoting the provider field in the database.
|
||||
FieldProvider = "provider"
|
||||
// FieldCheckMode holds the string denoting the check_mode field in the database.
|
||||
FieldCheckMode = "check_mode"
|
||||
// FieldAccountID holds the string denoting the account_id field in the database.
|
||||
FieldAccountID = "account_id"
|
||||
// FieldAPIMode holds the string denoting the api_mode field in the database.
|
||||
FieldAPIMode = "api_mode"
|
||||
// FieldEndpoint holds the string denoting the endpoint field in the database.
|
||||
@@ -91,6 +95,8 @@ var Columns = []string{
|
||||
FieldUpdatedAt,
|
||||
FieldName,
|
||||
FieldProvider,
|
||||
FieldCheckMode,
|
||||
FieldAccountID,
|
||||
FieldAPIMode,
|
||||
FieldEndpoint,
|
||||
FieldAPIKeyEncrypted,
|
||||
@@ -127,6 +133,10 @@ var (
|
||||
UpdateDefaultUpdatedAt func() time.Time
|
||||
// NameValidator is a validator for the "name" field. It is called by the builders before save.
|
||||
NameValidator func(string) error
|
||||
// DefaultCheckMode holds the default value on creation for the "check_mode" field.
|
||||
DefaultCheckMode string
|
||||
// CheckModeValidator is a validator for the "check_mode" field. It is called by the builders before save.
|
||||
CheckModeValidator func(string) error
|
||||
// DefaultAPIMode holds the default value on creation for the "api_mode" field.
|
||||
DefaultAPIMode string
|
||||
// APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save.
|
||||
@@ -164,10 +174,14 @@ type Provider string
|
||||
|
||||
// Provider values.
|
||||
const (
|
||||
ProviderOpenai Provider = "openai"
|
||||
ProviderAnthropic Provider = "anthropic"
|
||||
ProviderGemini Provider = "gemini"
|
||||
ProviderGrok Provider = "grok"
|
||||
ProviderOpenai Provider = "openai"
|
||||
ProviderAnthropic Provider = "anthropic"
|
||||
ProviderGemini Provider = "gemini"
|
||||
ProviderGrok Provider = "grok"
|
||||
ProviderAntigravity Provider = "antigravity"
|
||||
ProviderKimi Provider = "kimi"
|
||||
ProviderZhipu Provider = "zhipu"
|
||||
ProviderDeepseek Provider = "deepseek"
|
||||
)
|
||||
|
||||
func (pr Provider) String() string {
|
||||
@@ -177,7 +191,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:
|
||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("channelmonitor: invalid enum value for provider field: %q", pr)
|
||||
@@ -212,6 +226,16 @@ func ByProvider(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldProvider, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCheckMode orders the results by the check_mode field.
|
||||
func ByCheckMode(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCheckMode, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAccountID orders the results by the account_id field.
|
||||
func ByAccountID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAccountID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAPIMode orders the results by the api_mode field.
|
||||
func ByAPIMode(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAPIMode, opts...).ToFunc()
|
||||
|
||||
@@ -70,6 +70,16 @@ func Name(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEQ(FieldName, v))
|
||||
}
|
||||
|
||||
// CheckMode applies equality check predicate on the "check_mode" field. It's identical to CheckModeEQ.
|
||||
func CheckMode(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEQ(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// AccountID applies equality check predicate on the "account_id" field. It's identical to AccountIDEQ.
|
||||
func AccountID(v int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEQ(FieldAccountID, v))
|
||||
}
|
||||
|
||||
// APIMode applies equality check predicate on the "api_mode" field. It's identical to APIModeEQ.
|
||||
func APIMode(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEQ(FieldAPIMode, v))
|
||||
@@ -295,6 +305,121 @@ func ProviderNotIn(vs ...Provider) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldNotIn(FieldProvider, vs...))
|
||||
}
|
||||
|
||||
// CheckModeEQ applies the EQ predicate on the "check_mode" field.
|
||||
func CheckModeEQ(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEQ(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeNEQ applies the NEQ predicate on the "check_mode" field.
|
||||
func CheckModeNEQ(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldNEQ(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeIn applies the In predicate on the "check_mode" field.
|
||||
func CheckModeIn(vs ...string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldIn(FieldCheckMode, vs...))
|
||||
}
|
||||
|
||||
// CheckModeNotIn applies the NotIn predicate on the "check_mode" field.
|
||||
func CheckModeNotIn(vs ...string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldNotIn(FieldCheckMode, vs...))
|
||||
}
|
||||
|
||||
// CheckModeGT applies the GT predicate on the "check_mode" field.
|
||||
func CheckModeGT(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldGT(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeGTE applies the GTE predicate on the "check_mode" field.
|
||||
func CheckModeGTE(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldGTE(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeLT applies the LT predicate on the "check_mode" field.
|
||||
func CheckModeLT(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldLT(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeLTE applies the LTE predicate on the "check_mode" field.
|
||||
func CheckModeLTE(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldLTE(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeContains applies the Contains predicate on the "check_mode" field.
|
||||
func CheckModeContains(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldContains(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeHasPrefix applies the HasPrefix predicate on the "check_mode" field.
|
||||
func CheckModeHasPrefix(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldHasPrefix(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeHasSuffix applies the HasSuffix predicate on the "check_mode" field.
|
||||
func CheckModeHasSuffix(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldHasSuffix(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeEqualFold applies the EqualFold predicate on the "check_mode" field.
|
||||
func CheckModeEqualFold(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEqualFold(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// CheckModeContainsFold applies the ContainsFold predicate on the "check_mode" field.
|
||||
func CheckModeContainsFold(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldContainsFold(FieldCheckMode, v))
|
||||
}
|
||||
|
||||
// AccountIDEQ applies the EQ predicate on the "account_id" field.
|
||||
func AccountIDEQ(v int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEQ(FieldAccountID, v))
|
||||
}
|
||||
|
||||
// AccountIDNEQ applies the NEQ predicate on the "account_id" field.
|
||||
func AccountIDNEQ(v int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldNEQ(FieldAccountID, v))
|
||||
}
|
||||
|
||||
// AccountIDIn applies the In predicate on the "account_id" field.
|
||||
func AccountIDIn(vs ...int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldIn(FieldAccountID, vs...))
|
||||
}
|
||||
|
||||
// AccountIDNotIn applies the NotIn predicate on the "account_id" field.
|
||||
func AccountIDNotIn(vs ...int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldNotIn(FieldAccountID, vs...))
|
||||
}
|
||||
|
||||
// AccountIDGT applies the GT predicate on the "account_id" field.
|
||||
func AccountIDGT(v int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldGT(FieldAccountID, v))
|
||||
}
|
||||
|
||||
// AccountIDGTE applies the GTE predicate on the "account_id" field.
|
||||
func AccountIDGTE(v int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldGTE(FieldAccountID, v))
|
||||
}
|
||||
|
||||
// AccountIDLT applies the LT predicate on the "account_id" field.
|
||||
func AccountIDLT(v int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldLT(FieldAccountID, v))
|
||||
}
|
||||
|
||||
// AccountIDLTE applies the LTE predicate on the "account_id" field.
|
||||
func AccountIDLTE(v int64) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldLTE(FieldAccountID, v))
|
||||
}
|
||||
|
||||
// AccountIDIsNil applies the IsNil predicate on the "account_id" field.
|
||||
func AccountIDIsNil() predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldIsNull(FieldAccountID))
|
||||
}
|
||||
|
||||
// AccountIDNotNil applies the NotNil predicate on the "account_id" field.
|
||||
func AccountIDNotNil() predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldNotNull(FieldAccountID))
|
||||
}
|
||||
|
||||
// APIModeEQ applies the EQ predicate on the "api_mode" field.
|
||||
func APIModeEQ(v string) predicate.ChannelMonitor {
|
||||
return predicate.ChannelMonitor(sql.FieldEQ(FieldAPIMode, v))
|
||||
|
||||
@@ -65,6 +65,34 @@ func (_c *ChannelMonitorCreate) SetProvider(v channelmonitor.Provider) *ChannelM
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetCheckMode sets the "check_mode" field.
|
||||
func (_c *ChannelMonitorCreate) SetCheckMode(v string) *ChannelMonitorCreate {
|
||||
_c.mutation.SetCheckMode(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableCheckMode sets the "check_mode" field if the given value is not nil.
|
||||
func (_c *ChannelMonitorCreate) SetNillableCheckMode(v *string) *ChannelMonitorCreate {
|
||||
if v != nil {
|
||||
_c.SetCheckMode(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAccountID sets the "account_id" field.
|
||||
func (_c *ChannelMonitorCreate) SetAccountID(v int64) *ChannelMonitorCreate {
|
||||
_c.mutation.SetAccountID(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableAccountID sets the "account_id" field if the given value is not nil.
|
||||
func (_c *ChannelMonitorCreate) SetNillableAccountID(v *int64) *ChannelMonitorCreate {
|
||||
if v != nil {
|
||||
_c.SetAccountID(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAPIMode sets the "api_mode" field.
|
||||
func (_c *ChannelMonitorCreate) SetAPIMode(v string) *ChannelMonitorCreate {
|
||||
_c.mutation.SetAPIMode(v)
|
||||
@@ -303,6 +331,10 @@ func (_c *ChannelMonitorCreate) defaults() {
|
||||
v := channelmonitor.DefaultUpdatedAt()
|
||||
_c.mutation.SetUpdatedAt(v)
|
||||
}
|
||||
if _, ok := _c.mutation.CheckMode(); !ok {
|
||||
v := channelmonitor.DefaultCheckMode
|
||||
_c.mutation.SetCheckMode(v)
|
||||
}
|
||||
if _, ok := _c.mutation.APIMode(); !ok {
|
||||
v := channelmonitor.DefaultAPIMode
|
||||
_c.mutation.SetAPIMode(v)
|
||||
@@ -357,6 +389,14 @@ func (_c *ChannelMonitorCreate) check() error {
|
||||
return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.CheckMode(); !ok {
|
||||
return &ValidationError{Name: "check_mode", err: errors.New(`ent: missing required field "ChannelMonitor.check_mode"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.CheckMode(); ok {
|
||||
if err := channelmonitor.CheckModeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "check_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.check_mode": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.APIMode(); !ok {
|
||||
return &ValidationError{Name: "api_mode", err: errors.New(`ent: missing required field "ChannelMonitor.api_mode"`)}
|
||||
}
|
||||
@@ -473,6 +513,14 @@ func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateS
|
||||
_spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value)
|
||||
_node.Provider = value
|
||||
}
|
||||
if value, ok := _c.mutation.CheckMode(); ok {
|
||||
_spec.SetField(channelmonitor.FieldCheckMode, field.TypeString, value)
|
||||
_node.CheckMode = value
|
||||
}
|
||||
if value, ok := _c.mutation.AccountID(); ok {
|
||||
_spec.SetField(channelmonitor.FieldAccountID, field.TypeInt64, value)
|
||||
_node.AccountID = &value
|
||||
}
|
||||
if value, ok := _c.mutation.APIMode(); ok {
|
||||
_spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value)
|
||||
_node.APIMode = value
|
||||
@@ -666,6 +714,42 @@ func (u *ChannelMonitorUpsert) UpdateProvider() *ChannelMonitorUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetCheckMode sets the "check_mode" field.
|
||||
func (u *ChannelMonitorUpsert) SetCheckMode(v string) *ChannelMonitorUpsert {
|
||||
u.Set(channelmonitor.FieldCheckMode, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateCheckMode sets the "check_mode" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorUpsert) UpdateCheckMode() *ChannelMonitorUpsert {
|
||||
u.SetExcluded(channelmonitor.FieldCheckMode)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetAccountID sets the "account_id" field.
|
||||
func (u *ChannelMonitorUpsert) SetAccountID(v int64) *ChannelMonitorUpsert {
|
||||
u.Set(channelmonitor.FieldAccountID, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateAccountID sets the "account_id" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorUpsert) UpdateAccountID() *ChannelMonitorUpsert {
|
||||
u.SetExcluded(channelmonitor.FieldAccountID)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddAccountID adds v to the "account_id" field.
|
||||
func (u *ChannelMonitorUpsert) AddAccountID(v int64) *ChannelMonitorUpsert {
|
||||
u.Add(channelmonitor.FieldAccountID, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearAccountID clears the value of the "account_id" field.
|
||||
func (u *ChannelMonitorUpsert) ClearAccountID() *ChannelMonitorUpsert {
|
||||
u.SetNull(channelmonitor.FieldAccountID)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetAPIMode sets the "api_mode" field.
|
||||
func (u *ChannelMonitorUpsert) SetAPIMode(v string) *ChannelMonitorUpsert {
|
||||
u.Set(channelmonitor.FieldAPIMode, v)
|
||||
@@ -975,6 +1059,48 @@ func (u *ChannelMonitorUpsertOne) UpdateProvider() *ChannelMonitorUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetCheckMode sets the "check_mode" field.
|
||||
func (u *ChannelMonitorUpsertOne) SetCheckMode(v string) *ChannelMonitorUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.SetCheckMode(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCheckMode sets the "check_mode" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorUpsertOne) UpdateCheckMode() *ChannelMonitorUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.UpdateCheckMode()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAccountID sets the "account_id" field.
|
||||
func (u *ChannelMonitorUpsertOne) SetAccountID(v int64) *ChannelMonitorUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.SetAccountID(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAccountID adds v to the "account_id" field.
|
||||
func (u *ChannelMonitorUpsertOne) AddAccountID(v int64) *ChannelMonitorUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.AddAccountID(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAccountID sets the "account_id" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorUpsertOne) UpdateAccountID() *ChannelMonitorUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.UpdateAccountID()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAccountID clears the value of the "account_id" field.
|
||||
func (u *ChannelMonitorUpsertOne) ClearAccountID() *ChannelMonitorUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.ClearAccountID()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAPIMode sets the "api_mode" field.
|
||||
func (u *ChannelMonitorUpsertOne) SetAPIMode(v string) *ChannelMonitorUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
@@ -1487,6 +1613,48 @@ func (u *ChannelMonitorUpsertBulk) UpdateProvider() *ChannelMonitorUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetCheckMode sets the "check_mode" field.
|
||||
func (u *ChannelMonitorUpsertBulk) SetCheckMode(v string) *ChannelMonitorUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.SetCheckMode(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCheckMode sets the "check_mode" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorUpsertBulk) UpdateCheckMode() *ChannelMonitorUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.UpdateCheckMode()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAccountID sets the "account_id" field.
|
||||
func (u *ChannelMonitorUpsertBulk) SetAccountID(v int64) *ChannelMonitorUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.SetAccountID(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAccountID adds v to the "account_id" field.
|
||||
func (u *ChannelMonitorUpsertBulk) AddAccountID(v int64) *ChannelMonitorUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.AddAccountID(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAccountID sets the "account_id" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorUpsertBulk) UpdateAccountID() *ChannelMonitorUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.UpdateAccountID()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAccountID clears the value of the "account_id" field.
|
||||
func (u *ChannelMonitorUpsertBulk) ClearAccountID() *ChannelMonitorUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
s.ClearAccountID()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAPIMode sets the "api_mode" field.
|
||||
func (u *ChannelMonitorUpsertBulk) SetAPIMode(v string) *ChannelMonitorUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorUpsert) {
|
||||
|
||||
@@ -66,6 +66,47 @@ func (_u *ChannelMonitorUpdate) SetNillableProvider(v *channelmonitor.Provider)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCheckMode sets the "check_mode" field.
|
||||
func (_u *ChannelMonitorUpdate) SetCheckMode(v string) *ChannelMonitorUpdate {
|
||||
_u.mutation.SetCheckMode(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableCheckMode sets the "check_mode" field if the given value is not nil.
|
||||
func (_u *ChannelMonitorUpdate) SetNillableCheckMode(v *string) *ChannelMonitorUpdate {
|
||||
if v != nil {
|
||||
_u.SetCheckMode(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAccountID sets the "account_id" field.
|
||||
func (_u *ChannelMonitorUpdate) SetAccountID(v int64) *ChannelMonitorUpdate {
|
||||
_u.mutation.ResetAccountID()
|
||||
_u.mutation.SetAccountID(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAccountID sets the "account_id" field if the given value is not nil.
|
||||
func (_u *ChannelMonitorUpdate) SetNillableAccountID(v *int64) *ChannelMonitorUpdate {
|
||||
if v != nil {
|
||||
_u.SetAccountID(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAccountID adds value to the "account_id" field.
|
||||
func (_u *ChannelMonitorUpdate) AddAccountID(v int64) *ChannelMonitorUpdate {
|
||||
_u.mutation.AddAccountID(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAccountID clears the value of the "account_id" field.
|
||||
func (_u *ChannelMonitorUpdate) ClearAccountID() *ChannelMonitorUpdate {
|
||||
_u.mutation.ClearAccountID()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAPIMode sets the "api_mode" field.
|
||||
func (_u *ChannelMonitorUpdate) SetAPIMode(v string) *ChannelMonitorUpdate {
|
||||
_u.mutation.SetAPIMode(v)
|
||||
@@ -453,6 +494,11 @@ func (_u *ChannelMonitorUpdate) check() error {
|
||||
return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.CheckMode(); ok {
|
||||
if err := channelmonitor.CheckModeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "check_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.check_mode": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.APIMode(); ok {
|
||||
if err := channelmonitor.APIModeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_mode": %w`, err)}
|
||||
@@ -517,6 +563,18 @@ func (_u *ChannelMonitorUpdate) sqlSave(ctx context.Context) (_node int, err err
|
||||
if value, ok := _u.mutation.Provider(); ok {
|
||||
_spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value)
|
||||
}
|
||||
if value, ok := _u.mutation.CheckMode(); ok {
|
||||
_spec.SetField(channelmonitor.FieldCheckMode, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AccountID(); ok {
|
||||
_spec.SetField(channelmonitor.FieldAccountID, field.TypeInt64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAccountID(); ok {
|
||||
_spec.AddField(channelmonitor.FieldAccountID, field.TypeInt64, value)
|
||||
}
|
||||
if _u.mutation.AccountIDCleared() {
|
||||
_spec.ClearField(channelmonitor.FieldAccountID, field.TypeInt64)
|
||||
}
|
||||
if value, ok := _u.mutation.APIMode(); ok {
|
||||
_spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value)
|
||||
}
|
||||
@@ -755,6 +813,47 @@ func (_u *ChannelMonitorUpdateOne) SetNillableProvider(v *channelmonitor.Provide
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCheckMode sets the "check_mode" field.
|
||||
func (_u *ChannelMonitorUpdateOne) SetCheckMode(v string) *ChannelMonitorUpdateOne {
|
||||
_u.mutation.SetCheckMode(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableCheckMode sets the "check_mode" field if the given value is not nil.
|
||||
func (_u *ChannelMonitorUpdateOne) SetNillableCheckMode(v *string) *ChannelMonitorUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetCheckMode(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAccountID sets the "account_id" field.
|
||||
func (_u *ChannelMonitorUpdateOne) SetAccountID(v int64) *ChannelMonitorUpdateOne {
|
||||
_u.mutation.ResetAccountID()
|
||||
_u.mutation.SetAccountID(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAccountID sets the "account_id" field if the given value is not nil.
|
||||
func (_u *ChannelMonitorUpdateOne) SetNillableAccountID(v *int64) *ChannelMonitorUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetAccountID(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAccountID adds value to the "account_id" field.
|
||||
func (_u *ChannelMonitorUpdateOne) AddAccountID(v int64) *ChannelMonitorUpdateOne {
|
||||
_u.mutation.AddAccountID(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAccountID clears the value of the "account_id" field.
|
||||
func (_u *ChannelMonitorUpdateOne) ClearAccountID() *ChannelMonitorUpdateOne {
|
||||
_u.mutation.ClearAccountID()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAPIMode sets the "api_mode" field.
|
||||
func (_u *ChannelMonitorUpdateOne) SetAPIMode(v string) *ChannelMonitorUpdateOne {
|
||||
_u.mutation.SetAPIMode(v)
|
||||
@@ -1155,6 +1254,11 @@ func (_u *ChannelMonitorUpdateOne) check() error {
|
||||
return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.CheckMode(); ok {
|
||||
if err := channelmonitor.CheckModeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "check_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.check_mode": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.APIMode(); ok {
|
||||
if err := channelmonitor.APIModeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_mode": %w`, err)}
|
||||
@@ -1236,6 +1340,18 @@ func (_u *ChannelMonitorUpdateOne) sqlSave(ctx context.Context) (_node *ChannelM
|
||||
if value, ok := _u.mutation.Provider(); ok {
|
||||
_spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value)
|
||||
}
|
||||
if value, ok := _u.mutation.CheckMode(); ok {
|
||||
_spec.SetField(channelmonitor.FieldCheckMode, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AccountID(); ok {
|
||||
_spec.SetField(channelmonitor.FieldAccountID, field.TypeInt64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAccountID(); ok {
|
||||
_spec.AddField(channelmonitor.FieldAccountID, field.TypeInt64, value)
|
||||
}
|
||||
if _u.mutation.AccountIDCleared() {
|
||||
_spec.ClearField(channelmonitor.FieldAccountID, field.TypeInt64)
|
||||
}
|
||||
if value, ok := _u.mutation.APIMode(); ok {
|
||||
_spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package ent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
)
|
||||
|
||||
// ChannelMonitorHistory is the model entity for the ChannelMonitorHistory schema.
|
||||
@@ -30,6 +32,8 @@ type ChannelMonitorHistory struct {
|
||||
PingLatencyMs *int `json:"ping_latency_ms,omitempty"`
|
||||
// Message holds the value of the "message" field.
|
||||
Message string `json:"message,omitempty"`
|
||||
// Quota holds the value of the "quota" field.
|
||||
Quota *domain.MonitorQuotaSnapshot `json:"quota,omitempty"`
|
||||
// CheckedAt holds the value of the "checked_at" field.
|
||||
CheckedAt time.Time `json:"checked_at,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
@@ -63,6 +67,8 @@ func (*ChannelMonitorHistory) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case channelmonitorhistory.FieldQuota:
|
||||
values[i] = new([]byte)
|
||||
case channelmonitorhistory.FieldID, channelmonitorhistory.FieldMonitorID, channelmonitorhistory.FieldLatencyMs, channelmonitorhistory.FieldPingLatencyMs:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case channelmonitorhistory.FieldModel, channelmonitorhistory.FieldStatus, channelmonitorhistory.FieldMessage:
|
||||
@@ -128,6 +134,14 @@ func (_m *ChannelMonitorHistory) assignValues(columns []string, values []any) er
|
||||
} else if value.Valid {
|
||||
_m.Message = value.String
|
||||
}
|
||||
case channelmonitorhistory.FieldQuota:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field quota", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &_m.Quota); err != nil {
|
||||
return fmt.Errorf("unmarshal field quota: %w", err)
|
||||
}
|
||||
}
|
||||
case channelmonitorhistory.FieldCheckedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field checked_at", values[i])
|
||||
@@ -197,6 +211,9 @@ func (_m *ChannelMonitorHistory) String() string {
|
||||
builder.WriteString("message=")
|
||||
builder.WriteString(_m.Message)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("quota=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Quota))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("checked_at=")
|
||||
builder.WriteString(_m.CheckedAt.Format(time.ANSIC))
|
||||
builder.WriteByte(')')
|
||||
|
||||
@@ -27,6 +27,8 @@ const (
|
||||
FieldPingLatencyMs = "ping_latency_ms"
|
||||
// FieldMessage holds the string denoting the message field in the database.
|
||||
FieldMessage = "message"
|
||||
// FieldQuota holds the string denoting the quota field in the database.
|
||||
FieldQuota = "quota"
|
||||
// FieldCheckedAt holds the string denoting the checked_at field in the database.
|
||||
FieldCheckedAt = "checked_at"
|
||||
// EdgeMonitor holds the string denoting the monitor edge name in mutations.
|
||||
@@ -51,6 +53,7 @@ var Columns = []string{
|
||||
FieldLatencyMs,
|
||||
FieldPingLatencyMs,
|
||||
FieldMessage,
|
||||
FieldQuota,
|
||||
FieldCheckedAt,
|
||||
}
|
||||
|
||||
|
||||
@@ -365,6 +365,16 @@ func MessageContainsFold(v string) predicate.ChannelMonitorHistory {
|
||||
return predicate.ChannelMonitorHistory(sql.FieldContainsFold(FieldMessage, v))
|
||||
}
|
||||
|
||||
// QuotaIsNil applies the IsNil predicate on the "quota" field.
|
||||
func QuotaIsNil() predicate.ChannelMonitorHistory {
|
||||
return predicate.ChannelMonitorHistory(sql.FieldIsNull(FieldQuota))
|
||||
}
|
||||
|
||||
// QuotaNotNil applies the NotNil predicate on the "quota" field.
|
||||
func QuotaNotNil() predicate.ChannelMonitorHistory {
|
||||
return predicate.ChannelMonitorHistory(sql.FieldNotNull(FieldQuota))
|
||||
}
|
||||
|
||||
// CheckedAtEQ applies the EQ predicate on the "checked_at" field.
|
||||
func CheckedAtEQ(v time.Time) predicate.ChannelMonitorHistory {
|
||||
return predicate.ChannelMonitorHistory(sql.FieldEQ(FieldCheckedAt, v))
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"entgo.io/ent/schema/field"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
)
|
||||
|
||||
// ChannelMonitorHistoryCreate is the builder for creating a ChannelMonitorHistory entity.
|
||||
@@ -83,6 +84,12 @@ func (_c *ChannelMonitorHistoryCreate) SetNillableMessage(v *string) *ChannelMon
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetQuota sets the "quota" field.
|
||||
func (_c *ChannelMonitorHistoryCreate) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryCreate {
|
||||
_c.mutation.SetQuota(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetCheckedAt sets the "checked_at" field.
|
||||
func (_c *ChannelMonitorHistoryCreate) SetCheckedAt(v time.Time) *ChannelMonitorHistoryCreate {
|
||||
_c.mutation.SetCheckedAt(v)
|
||||
@@ -226,6 +233,10 @@ func (_c *ChannelMonitorHistoryCreate) createSpec() (*ChannelMonitorHistory, *sq
|
||||
_spec.SetField(channelmonitorhistory.FieldMessage, field.TypeString, value)
|
||||
_node.Message = value
|
||||
}
|
||||
if value, ok := _c.mutation.Quota(); ok {
|
||||
_spec.SetField(channelmonitorhistory.FieldQuota, field.TypeJSON, value)
|
||||
_node.Quota = value
|
||||
}
|
||||
if value, ok := _c.mutation.CheckedAt(); ok {
|
||||
_spec.SetField(channelmonitorhistory.FieldCheckedAt, field.TypeTime, value)
|
||||
_node.CheckedAt = value
|
||||
@@ -401,6 +412,24 @@ func (u *ChannelMonitorHistoryUpsert) ClearMessage() *ChannelMonitorHistoryUpser
|
||||
return u
|
||||
}
|
||||
|
||||
// SetQuota sets the "quota" field.
|
||||
func (u *ChannelMonitorHistoryUpsert) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpsert {
|
||||
u.Set(channelmonitorhistory.FieldQuota, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateQuota sets the "quota" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorHistoryUpsert) UpdateQuota() *ChannelMonitorHistoryUpsert {
|
||||
u.SetExcluded(channelmonitorhistory.FieldQuota)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearQuota clears the value of the "quota" field.
|
||||
func (u *ChannelMonitorHistoryUpsert) ClearQuota() *ChannelMonitorHistoryUpsert {
|
||||
u.SetNull(channelmonitorhistory.FieldQuota)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetCheckedAt sets the "checked_at" field.
|
||||
func (u *ChannelMonitorHistoryUpsert) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpsert {
|
||||
u.Set(channelmonitorhistory.FieldCheckedAt, v)
|
||||
@@ -572,6 +601,27 @@ func (u *ChannelMonitorHistoryUpsertOne) ClearMessage() *ChannelMonitorHistoryUp
|
||||
})
|
||||
}
|
||||
|
||||
// SetQuota sets the "quota" field.
|
||||
func (u *ChannelMonitorHistoryUpsertOne) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
s.SetQuota(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateQuota sets the "quota" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorHistoryUpsertOne) UpdateQuota() *ChannelMonitorHistoryUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
s.UpdateQuota()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearQuota clears the value of the "quota" field.
|
||||
func (u *ChannelMonitorHistoryUpsertOne) ClearQuota() *ChannelMonitorHistoryUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
s.ClearQuota()
|
||||
})
|
||||
}
|
||||
|
||||
// SetCheckedAt sets the "checked_at" field.
|
||||
func (u *ChannelMonitorHistoryUpsertOne) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpsertOne {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
@@ -909,6 +959,27 @@ func (u *ChannelMonitorHistoryUpsertBulk) ClearMessage() *ChannelMonitorHistoryU
|
||||
})
|
||||
}
|
||||
|
||||
// SetQuota sets the "quota" field.
|
||||
func (u *ChannelMonitorHistoryUpsertBulk) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
s.SetQuota(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateQuota sets the "quota" field to the value that was provided on create.
|
||||
func (u *ChannelMonitorHistoryUpsertBulk) UpdateQuota() *ChannelMonitorHistoryUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
s.UpdateQuota()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearQuota clears the value of the "quota" field.
|
||||
func (u *ChannelMonitorHistoryUpsertBulk) ClearQuota() *ChannelMonitorHistoryUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
s.ClearQuota()
|
||||
})
|
||||
}
|
||||
|
||||
// SetCheckedAt sets the "checked_at" field.
|
||||
func (u *ChannelMonitorHistoryUpsertBulk) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpsertBulk {
|
||||
return u.Update(func(s *ChannelMonitorHistoryUpsert) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
)
|
||||
|
||||
// ChannelMonitorHistoryUpdate is the builder for updating ChannelMonitorHistory entities.
|
||||
@@ -145,6 +146,18 @@ func (_u *ChannelMonitorHistoryUpdate) ClearMessage() *ChannelMonitorHistoryUpda
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetQuota sets the "quota" field.
|
||||
func (_u *ChannelMonitorHistoryUpdate) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpdate {
|
||||
_u.mutation.SetQuota(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearQuota clears the value of the "quota" field.
|
||||
func (_u *ChannelMonitorHistoryUpdate) ClearQuota() *ChannelMonitorHistoryUpdate {
|
||||
_u.mutation.ClearQuota()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCheckedAt sets the "checked_at" field.
|
||||
func (_u *ChannelMonitorHistoryUpdate) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpdate {
|
||||
_u.mutation.SetCheckedAt(v)
|
||||
@@ -267,6 +280,12 @@ func (_u *ChannelMonitorHistoryUpdate) sqlSave(ctx context.Context) (_node int,
|
||||
if _u.mutation.MessageCleared() {
|
||||
_spec.ClearField(channelmonitorhistory.FieldMessage, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Quota(); ok {
|
||||
_spec.SetField(channelmonitorhistory.FieldQuota, field.TypeJSON, value)
|
||||
}
|
||||
if _u.mutation.QuotaCleared() {
|
||||
_spec.ClearField(channelmonitorhistory.FieldQuota, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.CheckedAt(); ok {
|
||||
_spec.SetField(channelmonitorhistory.FieldCheckedAt, field.TypeTime, value)
|
||||
}
|
||||
@@ -435,6 +454,18 @@ func (_u *ChannelMonitorHistoryUpdateOne) ClearMessage() *ChannelMonitorHistoryU
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetQuota sets the "quota" field.
|
||||
func (_u *ChannelMonitorHistoryUpdateOne) SetQuota(v *domain.MonitorQuotaSnapshot) *ChannelMonitorHistoryUpdateOne {
|
||||
_u.mutation.SetQuota(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearQuota clears the value of the "quota" field.
|
||||
func (_u *ChannelMonitorHistoryUpdateOne) ClearQuota() *ChannelMonitorHistoryUpdateOne {
|
||||
_u.mutation.ClearQuota()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCheckedAt sets the "checked_at" field.
|
||||
func (_u *ChannelMonitorHistoryUpdateOne) SetCheckedAt(v time.Time) *ChannelMonitorHistoryUpdateOne {
|
||||
_u.mutation.SetCheckedAt(v)
|
||||
@@ -587,6 +618,12 @@ func (_u *ChannelMonitorHistoryUpdateOne) sqlSave(ctx context.Context) (_node *C
|
||||
if _u.mutation.MessageCleared() {
|
||||
_spec.ClearField(channelmonitorhistory.FieldMessage, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.Quota(); ok {
|
||||
_spec.SetField(channelmonitorhistory.FieldQuota, field.TypeJSON, value)
|
||||
}
|
||||
if _u.mutation.QuotaCleared() {
|
||||
_spec.ClearField(channelmonitorhistory.FieldQuota, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.CheckedAt(); ok {
|
||||
_spec.SetField(channelmonitorhistory.FieldCheckedAt, field.TypeTime, value)
|
||||
}
|
||||
|
||||
@@ -100,10 +100,14 @@ type Provider string
|
||||
|
||||
// Provider values.
|
||||
const (
|
||||
ProviderOpenai Provider = "openai"
|
||||
ProviderAnthropic Provider = "anthropic"
|
||||
ProviderGemini Provider = "gemini"
|
||||
ProviderGrok Provider = "grok"
|
||||
ProviderOpenai Provider = "openai"
|
||||
ProviderAnthropic Provider = "anthropic"
|
||||
ProviderGemini Provider = "gemini"
|
||||
ProviderGrok Provider = "grok"
|
||||
ProviderAntigravity Provider = "antigravity"
|
||||
ProviderKimi Provider = "kimi"
|
||||
ProviderZhipu Provider = "zhipu"
|
||||
ProviderDeepseek Provider = "deepseek"
|
||||
)
|
||||
|
||||
func (pr Provider) String() string {
|
||||
@@ -113,7 +117,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:
|
||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderGrok, ProviderAntigravity, ProviderKimi, ProviderZhipu, ProviderDeepseek:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
|
||||
|
||||
@@ -6828,25 +6828,25 @@ type (
|
||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead, AuthIdentity,
|
||||
AuthIdentityChannel, BatchImageEvent, BatchImageItem, BatchImageJob,
|
||||
ChannelMonitor, ChannelMonitorDailyRollup, ChannelMonitorHistory,
|
||||
ChannelMonitorRequestTemplate, CompositeModelRoute,
|
||||
ErrorPassthroughRule, Group, IdempotencyRecord, IdentityAdoptionDecision,
|
||||
PaymentAuditLog, PaymentOrder, PaymentProviderInstance, PendingAuthSession,
|
||||
PromoCode, PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting,
|
||||
SubscriptionPlan, TLSFingerprintProfile, UsageCleanupTask, UsageLog, User,
|
||||
UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
|
||||
UserPlatformQuota, UserSubscription []ent.Hook
|
||||
ChannelMonitorRequestTemplate, CompositeModelRoute, ErrorPassthroughRule,
|
||||
Group, IdempotencyRecord, IdentityAdoptionDecision, PaymentAuditLog,
|
||||
PaymentOrder, PaymentProviderInstance, PendingAuthSession, PromoCode,
|
||||
PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting, SubscriptionPlan,
|
||||
TLSFingerprintProfile, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
|
||||
UserAttributeDefinition, UserAttributeValue, UserPlatformQuota,
|
||||
UserSubscription []ent.Hook
|
||||
}
|
||||
inters struct {
|
||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead, AuthIdentity,
|
||||
AuthIdentityChannel, BatchImageEvent, BatchImageItem, BatchImageJob,
|
||||
ChannelMonitor, ChannelMonitorDailyRollup, ChannelMonitorHistory,
|
||||
ChannelMonitorRequestTemplate, CompositeModelRoute,
|
||||
ErrorPassthroughRule, Group, IdempotencyRecord, IdentityAdoptionDecision,
|
||||
PaymentAuditLog, PaymentOrder, PaymentProviderInstance, PendingAuthSession,
|
||||
PromoCode, PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting,
|
||||
SubscriptionPlan, TLSFingerprintProfile, UsageCleanupTask, UsageLog, User,
|
||||
UserAllowedGroup, UserAttributeDefinition, UserAttributeValue,
|
||||
UserPlatformQuota, UserSubscription []ent.Interceptor
|
||||
ChannelMonitorRequestTemplate, CompositeModelRoute, ErrorPassthroughRule,
|
||||
Group, IdempotencyRecord, IdentityAdoptionDecision, PaymentAuditLog,
|
||||
PaymentOrder, PaymentProviderInstance, PendingAuthSession, PromoCode,
|
||||
PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting, SubscriptionPlan,
|
||||
TLSFingerprintProfile, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
|
||||
UserAttributeDefinition, UserAttributeValue, UserPlatformQuota,
|
||||
UserSubscription []ent.Interceptor
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package ent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/json/jsontext"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -85,8 +86,22 @@ type Group struct {
|
||||
VideoPrice720p *float64 `json:"video_price_720p,omitempty"`
|
||||
// VideoPrice1080p holds the value of the "video_price_1080p" field.
|
||||
VideoPrice1080p *float64 `json:"video_price_1080p,omitempty"`
|
||||
// 按模型族和分辨率覆盖视频每秒价格
|
||||
VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"`
|
||||
// Codex alpha/search 网页搜索单次价格(USD/次);nil 表示使用默认价 0.01(官方 $10/1000 次)
|
||||
WebSearchPricePerCall *float64 `json:"web_search_price_per_call,omitempty"`
|
||||
// 搜索工具价格 per 1000 calls(web_search 等)
|
||||
SearchPricePer1k *float64 `json:"search_price_per_1k,omitempty"`
|
||||
// Voice realtime 每分钟价格(USD)
|
||||
AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min,omitempty"`
|
||||
// TTS 每百万字符价格(USD)
|
||||
AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars,omitempty"`
|
||||
// STT 每小时价格(USD)
|
||||
AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour,omitempty"`
|
||||
// 是否按上下文长度应用模型阶梯价格;默认开启以保持官方/渠道长上下文价
|
||||
LongContextPricingEnabled bool `json:"long_context_pricing_enabled,omitempty"`
|
||||
// 分组逐模型定价;优先级高于渠道和内置定价
|
||||
ModelPricing jsontext.Value `json:"model_pricing,omitempty"`
|
||||
// 是否仅允许 Claude Code 客户端
|
||||
ClaudeCodeOnly bool `json:"claude_code_only,omitempty"`
|
||||
// 非 Claude Code 请求降级使用的分组 ID
|
||||
@@ -123,6 +138,12 @@ type Group struct {
|
||||
MaxReasoningEffort string `json:"max_reasoning_effort,omitempty"`
|
||||
// OpenAI reasoning effort 自定义精确映射;先映射再应用上限
|
||||
ReasoningEffortMappings []domain.ReasoningEffortMapping `json:"reasoning_effort_mappings,omitempty"`
|
||||
// 是否启用利润控制:调度时仅允许账号计费倍率满足毛利率要求的账号进入候选池
|
||||
ProfitControlEnabled bool `json:"profit_control_enabled,omitempty"`
|
||||
// 最低毛利率,小数(0.30=30%);账号准入条件为 U <= D*(1-margin-buffer)
|
||||
ProfitMinMargin float64 `json:"profit_min_margin,omitempty"`
|
||||
// 安全缓冲,小数;与 margin 相加后从下游倍率中扣除,默认 0
|
||||
ProfitSafetyBuffer float64 `json:"profit_safety_buffer,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the GroupQuery when eager-loading is set.
|
||||
Edges GroupEdges `json:"edges"`
|
||||
@@ -229,11 +250,11 @@ func (*Group) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldReasoningEffortMappings:
|
||||
case group.FieldVideoModelPrices, group.FieldModelPricing, group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldReasoningEffortMappings:
|
||||
values[i] = new([]byte)
|
||||
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet:
|
||||
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:
|
||||
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:
|
||||
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)
|
||||
@@ -472,6 +493,14 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
_m.VideoPrice1080p = new(float64)
|
||||
*_m.VideoPrice1080p = value.Float64
|
||||
}
|
||||
case group.FieldVideoModelPrices:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field video_model_prices", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &_m.VideoModelPrices); err != nil {
|
||||
return fmt.Errorf("unmarshal field video_model_prices: %w", err)
|
||||
}
|
||||
}
|
||||
case group.FieldWebSearchPricePerCall:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field web_search_price_per_call", values[i])
|
||||
@@ -479,6 +508,48 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
_m.WebSearchPricePerCall = new(float64)
|
||||
*_m.WebSearchPricePerCall = value.Float64
|
||||
}
|
||||
case group.FieldSearchPricePer1k:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field search_price_per_1k", values[i])
|
||||
} else if value.Valid {
|
||||
_m.SearchPricePer1k = new(float64)
|
||||
*_m.SearchPricePer1k = value.Float64
|
||||
}
|
||||
case group.FieldAudioRealtimePricePerMin:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field audio_realtime_price_per_min", values[i])
|
||||
} else if value.Valid {
|
||||
_m.AudioRealtimePricePerMin = new(float64)
|
||||
*_m.AudioRealtimePricePerMin = value.Float64
|
||||
}
|
||||
case group.FieldAudioTtsPricePerMillionChars:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field audio_tts_price_per_million_chars", values[i])
|
||||
} else if value.Valid {
|
||||
_m.AudioTtsPricePerMillionChars = new(float64)
|
||||
*_m.AudioTtsPricePerMillionChars = value.Float64
|
||||
}
|
||||
case group.FieldAudioSttPricePerHour:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field audio_stt_price_per_hour", values[i])
|
||||
} else if value.Valid {
|
||||
_m.AudioSttPricePerHour = new(float64)
|
||||
*_m.AudioSttPricePerHour = value.Float64
|
||||
}
|
||||
case group.FieldLongContextPricingEnabled:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field long_context_pricing_enabled", values[i])
|
||||
} else if value.Valid {
|
||||
_m.LongContextPricingEnabled = value.Bool
|
||||
}
|
||||
case group.FieldModelPricing:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field model_pricing", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &_m.ModelPricing); err != nil {
|
||||
return fmt.Errorf("unmarshal field model_pricing: %w", err)
|
||||
}
|
||||
}
|
||||
case group.FieldClaudeCodeOnly:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field claude_code_only", values[i])
|
||||
@@ -599,6 +670,24 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
return fmt.Errorf("unmarshal field reasoning_effort_mappings: %w", err)
|
||||
}
|
||||
}
|
||||
case group.FieldProfitControlEnabled:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field profit_control_enabled", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ProfitControlEnabled = value.Bool
|
||||
}
|
||||
case group.FieldProfitMinMargin:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field profit_min_margin", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ProfitMinMargin = value.Float64
|
||||
}
|
||||
case group.FieldProfitSafetyBuffer:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field profit_safety_buffer", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ProfitSafetyBuffer = value.Float64
|
||||
}
|
||||
default:
|
||||
_m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
@@ -798,11 +887,40 @@ func (_m *Group) String() string {
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("video_model_prices=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.VideoModelPrices))
|
||||
builder.WriteString(", ")
|
||||
if v := _m.WebSearchPricePerCall; v != nil {
|
||||
builder.WriteString("web_search_price_per_call=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.SearchPricePer1k; v != nil {
|
||||
builder.WriteString("search_price_per_1k=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.AudioRealtimePricePerMin; v != nil {
|
||||
builder.WriteString("audio_realtime_price_per_min=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.AudioTtsPricePerMillionChars; v != nil {
|
||||
builder.WriteString("audio_tts_price_per_million_chars=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.AudioSttPricePerHour; v != nil {
|
||||
builder.WriteString("audio_stt_price_per_hour=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("long_context_pricing_enabled=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.LongContextPricingEnabled))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("model_pricing=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ModelPricing))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("claude_code_only=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ClaudeCodeOnly))
|
||||
builder.WriteString(", ")
|
||||
@@ -860,6 +978,15 @@ func (_m *Group) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("reasoning_effort_mappings=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ReasoningEffortMappings))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("profit_control_enabled=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ProfitControlEnabled))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("profit_min_margin=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ProfitMinMargin))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("profit_safety_buffer=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ProfitSafetyBuffer))
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -82,8 +82,22 @@ const (
|
||||
FieldVideoPrice720p = "video_price_720p"
|
||||
// FieldVideoPrice1080p holds the string denoting the video_price_1080p field in the database.
|
||||
FieldVideoPrice1080p = "video_price_1080p"
|
||||
// FieldVideoModelPrices holds the string denoting the video_model_prices field in the database.
|
||||
FieldVideoModelPrices = "video_model_prices"
|
||||
// FieldWebSearchPricePerCall holds the string denoting the web_search_price_per_call field in the database.
|
||||
FieldWebSearchPricePerCall = "web_search_price_per_call"
|
||||
// FieldSearchPricePer1k holds the string denoting the search_price_per_1k field in the database.
|
||||
FieldSearchPricePer1k = "search_price_per_1k"
|
||||
// FieldAudioRealtimePricePerMin holds the string denoting the audio_realtime_price_per_min field in the database.
|
||||
FieldAudioRealtimePricePerMin = "audio_realtime_price_per_min"
|
||||
// FieldAudioTtsPricePerMillionChars holds the string denoting the audio_tts_price_per_million_chars field in the database.
|
||||
FieldAudioTtsPricePerMillionChars = "audio_tts_price_per_million_chars"
|
||||
// FieldAudioSttPricePerHour holds the string denoting the audio_stt_price_per_hour field in the database.
|
||||
FieldAudioSttPricePerHour = "audio_stt_price_per_hour"
|
||||
// FieldLongContextPricingEnabled holds the string denoting the long_context_pricing_enabled field in the database.
|
||||
FieldLongContextPricingEnabled = "long_context_pricing_enabled"
|
||||
// FieldModelPricing holds the string denoting the model_pricing field in the database.
|
||||
FieldModelPricing = "model_pricing"
|
||||
// FieldClaudeCodeOnly holds the string denoting the claude_code_only field in the database.
|
||||
FieldClaudeCodeOnly = "claude_code_only"
|
||||
// FieldFallbackGroupID holds the string denoting the fallback_group_id field in the database.
|
||||
@@ -120,6 +134,12 @@ const (
|
||||
FieldMaxReasoningEffort = "max_reasoning_effort"
|
||||
// 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.
|
||||
FieldProfitControlEnabled = "profit_control_enabled"
|
||||
// FieldProfitMinMargin holds the string denoting the profit_min_margin field in the database.
|
||||
FieldProfitMinMargin = "profit_min_margin"
|
||||
// FieldProfitSafetyBuffer holds the string denoting the profit_safety_buffer field in the database.
|
||||
FieldProfitSafetyBuffer = "profit_safety_buffer"
|
||||
// EdgeAPIKeys holds the string denoting the api_keys edge name in mutations.
|
||||
EdgeAPIKeys = "api_keys"
|
||||
// EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations.
|
||||
@@ -228,7 +248,14 @@ var Columns = []string{
|
||||
FieldVideoPrice480p,
|
||||
FieldVideoPrice720p,
|
||||
FieldVideoPrice1080p,
|
||||
FieldVideoModelPrices,
|
||||
FieldWebSearchPricePerCall,
|
||||
FieldSearchPricePer1k,
|
||||
FieldAudioRealtimePricePerMin,
|
||||
FieldAudioTtsPricePerMillionChars,
|
||||
FieldAudioSttPricePerHour,
|
||||
FieldLongContextPricingEnabled,
|
||||
FieldModelPricing,
|
||||
FieldClaudeCodeOnly,
|
||||
FieldFallbackGroupID,
|
||||
FieldFallbackGroupIDOnInvalidRequest,
|
||||
@@ -247,6 +274,9 @@ var Columns = []string{
|
||||
FieldRpmLimit,
|
||||
FieldMaxReasoningEffort,
|
||||
FieldReasoningEffortMappings,
|
||||
FieldProfitControlEnabled,
|
||||
FieldProfitMinMargin,
|
||||
FieldProfitSafetyBuffer,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -332,6 +362,16 @@ var (
|
||||
DefaultVideoRateIndependent bool
|
||||
// DefaultVideoRateMultiplier holds the default value on creation for the "video_rate_multiplier" field.
|
||||
DefaultVideoRateMultiplier float64
|
||||
// SearchPricePer1kValidator is a validator for the "search_price_per_1k" field. It is called by the builders before save.
|
||||
SearchPricePer1kValidator func(float64) error
|
||||
// AudioRealtimePricePerMinValidator is a validator for the "audio_realtime_price_per_min" field. It is called by the builders before save.
|
||||
AudioRealtimePricePerMinValidator func(float64) error
|
||||
// AudioTtsPricePerMillionCharsValidator is a validator for the "audio_tts_price_per_million_chars" field. It is called by the builders before save.
|
||||
AudioTtsPricePerMillionCharsValidator func(float64) error
|
||||
// AudioSttPricePerHourValidator is a validator for the "audio_stt_price_per_hour" field. It is called by the builders before save.
|
||||
AudioSttPricePerHourValidator func(float64) error
|
||||
// DefaultLongContextPricingEnabled holds the default value on creation for the "long_context_pricing_enabled" field.
|
||||
DefaultLongContextPricingEnabled bool
|
||||
// DefaultClaudeCodeOnly holds the default value on creation for the "claude_code_only" field.
|
||||
DefaultClaudeCodeOnly bool
|
||||
// DefaultModelRoutingEnabled holds the default value on creation for the "model_routing_enabled" field.
|
||||
@@ -366,6 +406,12 @@ var (
|
||||
MaxReasoningEffortValidator 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.
|
||||
DefaultProfitControlEnabled bool
|
||||
// DefaultProfitMinMargin holds the default value on creation for the "profit_min_margin" field.
|
||||
DefaultProfitMinMargin float64
|
||||
// DefaultProfitSafetyBuffer holds the default value on creation for the "profit_safety_buffer" field.
|
||||
DefaultProfitSafetyBuffer float64
|
||||
)
|
||||
|
||||
// OrderOption defines the ordering options for the Group queries.
|
||||
@@ -546,6 +592,31 @@ func ByWebSearchPricePerCall(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldWebSearchPricePerCall, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// BySearchPricePer1k orders the results by the search_price_per_1k field.
|
||||
func BySearchPricePer1k(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldSearchPricePer1k, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAudioRealtimePricePerMin orders the results by the audio_realtime_price_per_min field.
|
||||
func ByAudioRealtimePricePerMin(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAudioRealtimePricePerMin, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAudioTtsPricePerMillionChars orders the results by the audio_tts_price_per_million_chars field.
|
||||
func ByAudioTtsPricePerMillionChars(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAudioTtsPricePerMillionChars, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAudioSttPricePerHour orders the results by the audio_stt_price_per_hour field.
|
||||
func ByAudioSttPricePerHour(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAudioSttPricePerHour, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByLongContextPricingEnabled orders the results by the long_context_pricing_enabled field.
|
||||
func ByLongContextPricingEnabled(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldLongContextPricingEnabled, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByClaudeCodeOnly orders the results by the claude_code_only field.
|
||||
func ByClaudeCodeOnly(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldClaudeCodeOnly, opts...).ToFunc()
|
||||
@@ -611,6 +682,21 @@ func ByMaxReasoningEffort(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldMaxReasoningEffort, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByProfitControlEnabled orders the results by the profit_control_enabled field.
|
||||
func ByProfitControlEnabled(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldProfitControlEnabled, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByProfitMinMargin orders the results by the profit_min_margin field.
|
||||
func ByProfitMinMargin(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldProfitMinMargin, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByProfitSafetyBuffer orders the results by the profit_safety_buffer field.
|
||||
func ByProfitSafetyBuffer(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldProfitSafetyBuffer, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAPIKeysCount orders the results by api_keys count.
|
||||
func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
|
||||
@@ -225,6 +225,31 @@ func WebSearchPricePerCall(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldWebSearchPricePerCall, v))
|
||||
}
|
||||
|
||||
// SearchPricePer1k applies equality check predicate on the "search_price_per_1k" field. It's identical to SearchPricePer1kEQ.
|
||||
func SearchPricePer1k(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldSearchPricePer1k, v))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMin applies equality check predicate on the "audio_realtime_price_per_min" field. It's identical to AudioRealtimePricePerMinEQ.
|
||||
func AudioRealtimePricePerMin(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAudioRealtimePricePerMin, v))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionChars applies equality check predicate on the "audio_tts_price_per_million_chars" field. It's identical to AudioTtsPricePerMillionCharsEQ.
|
||||
func AudioTtsPricePerMillionChars(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAudioTtsPricePerMillionChars, v))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHour applies equality check predicate on the "audio_stt_price_per_hour" field. It's identical to AudioSttPricePerHourEQ.
|
||||
func AudioSttPricePerHour(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAudioSttPricePerHour, v))
|
||||
}
|
||||
|
||||
// LongContextPricingEnabled applies equality check predicate on the "long_context_pricing_enabled" field. It's identical to LongContextPricingEnabledEQ.
|
||||
func LongContextPricingEnabled(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldLongContextPricingEnabled, v))
|
||||
}
|
||||
|
||||
// ClaudeCodeOnly applies equality check predicate on the "claude_code_only" field. It's identical to ClaudeCodeOnlyEQ.
|
||||
func ClaudeCodeOnly(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
|
||||
@@ -290,6 +315,21 @@ func MaxReasoningEffort(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffort, 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))
|
||||
}
|
||||
|
||||
// ProfitMinMargin applies equality check predicate on the "profit_min_margin" field. It's identical to ProfitMinMarginEQ.
|
||||
func ProfitMinMargin(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldProfitMinMargin, v))
|
||||
}
|
||||
|
||||
// ProfitSafetyBuffer applies equality check predicate on the "profit_safety_buffer" field. It's identical to ProfitSafetyBufferEQ.
|
||||
func ProfitSafetyBuffer(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldProfitSafetyBuffer, v))
|
||||
}
|
||||
|
||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||
func CreatedAtEQ(v time.Time) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldCreatedAt, v))
|
||||
@@ -1750,6 +1790,16 @@ func VideoPrice1080pNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldVideoPrice1080p))
|
||||
}
|
||||
|
||||
// VideoModelPricesIsNil applies the IsNil predicate on the "video_model_prices" field.
|
||||
func VideoModelPricesIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldVideoModelPrices))
|
||||
}
|
||||
|
||||
// VideoModelPricesNotNil applies the NotNil predicate on the "video_model_prices" field.
|
||||
func VideoModelPricesNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldVideoModelPrices))
|
||||
}
|
||||
|
||||
// WebSearchPricePerCallEQ applies the EQ predicate on the "web_search_price_per_call" field.
|
||||
func WebSearchPricePerCallEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldWebSearchPricePerCall, v))
|
||||
@@ -1800,6 +1850,226 @@ func WebSearchPricePerCallNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldWebSearchPricePerCall))
|
||||
}
|
||||
|
||||
// SearchPricePer1kEQ applies the EQ predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldSearchPricePer1k, v))
|
||||
}
|
||||
|
||||
// SearchPricePer1kNEQ applies the NEQ predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldSearchPricePer1k, v))
|
||||
}
|
||||
|
||||
// SearchPricePer1kIn applies the In predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldSearchPricePer1k, vs...))
|
||||
}
|
||||
|
||||
// SearchPricePer1kNotIn applies the NotIn predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldSearchPricePer1k, vs...))
|
||||
}
|
||||
|
||||
// SearchPricePer1kGT applies the GT predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldSearchPricePer1k, v))
|
||||
}
|
||||
|
||||
// SearchPricePer1kGTE applies the GTE predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldSearchPricePer1k, v))
|
||||
}
|
||||
|
||||
// SearchPricePer1kLT applies the LT predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldSearchPricePer1k, v))
|
||||
}
|
||||
|
||||
// SearchPricePer1kLTE applies the LTE predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldSearchPricePer1k, v))
|
||||
}
|
||||
|
||||
// SearchPricePer1kIsNil applies the IsNil predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldSearchPricePer1k))
|
||||
}
|
||||
|
||||
// SearchPricePer1kNotNil applies the NotNil predicate on the "search_price_per_1k" field.
|
||||
func SearchPricePer1kNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldSearchPricePer1k))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinEQ applies the EQ predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAudioRealtimePricePerMin, v))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinNEQ applies the NEQ predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldAudioRealtimePricePerMin, v))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinIn applies the In predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldAudioRealtimePricePerMin, vs...))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinNotIn applies the NotIn predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldAudioRealtimePricePerMin, vs...))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinGT applies the GT predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldAudioRealtimePricePerMin, v))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinGTE applies the GTE predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldAudioRealtimePricePerMin, v))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinLT applies the LT predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldAudioRealtimePricePerMin, v))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinLTE applies the LTE predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldAudioRealtimePricePerMin, v))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinIsNil applies the IsNil predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldAudioRealtimePricePerMin))
|
||||
}
|
||||
|
||||
// AudioRealtimePricePerMinNotNil applies the NotNil predicate on the "audio_realtime_price_per_min" field.
|
||||
func AudioRealtimePricePerMinNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldAudioRealtimePricePerMin))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsEQ applies the EQ predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAudioTtsPricePerMillionChars, v))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsNEQ applies the NEQ predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldAudioTtsPricePerMillionChars, v))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsIn applies the In predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldAudioTtsPricePerMillionChars, vs...))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsNotIn applies the NotIn predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldAudioTtsPricePerMillionChars, vs...))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsGT applies the GT predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldAudioTtsPricePerMillionChars, v))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsGTE applies the GTE predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldAudioTtsPricePerMillionChars, v))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsLT applies the LT predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldAudioTtsPricePerMillionChars, v))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsLTE applies the LTE predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldAudioTtsPricePerMillionChars, v))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsIsNil applies the IsNil predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldAudioTtsPricePerMillionChars))
|
||||
}
|
||||
|
||||
// AudioTtsPricePerMillionCharsNotNil applies the NotNil predicate on the "audio_tts_price_per_million_chars" field.
|
||||
func AudioTtsPricePerMillionCharsNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldAudioTtsPricePerMillionChars))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourEQ applies the EQ predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAudioSttPricePerHour, v))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourNEQ applies the NEQ predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldAudioSttPricePerHour, v))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourIn applies the In predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldAudioSttPricePerHour, vs...))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourNotIn applies the NotIn predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldAudioSttPricePerHour, vs...))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourGT applies the GT predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldAudioSttPricePerHour, v))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourGTE applies the GTE predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldAudioSttPricePerHour, v))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourLT applies the LT predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldAudioSttPricePerHour, v))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourLTE applies the LTE predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldAudioSttPricePerHour, v))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourIsNil applies the IsNil predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldAudioSttPricePerHour))
|
||||
}
|
||||
|
||||
// AudioSttPricePerHourNotNil applies the NotNil predicate on the "audio_stt_price_per_hour" field.
|
||||
func AudioSttPricePerHourNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldAudioSttPricePerHour))
|
||||
}
|
||||
|
||||
// LongContextPricingEnabledEQ applies the EQ predicate on the "long_context_pricing_enabled" field.
|
||||
func LongContextPricingEnabledEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldLongContextPricingEnabled, v))
|
||||
}
|
||||
|
||||
// LongContextPricingEnabledNEQ applies the NEQ predicate on the "long_context_pricing_enabled" field.
|
||||
func LongContextPricingEnabledNEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldLongContextPricingEnabled, v))
|
||||
}
|
||||
|
||||
// ModelPricingIsNil applies the IsNil predicate on the "model_pricing" field.
|
||||
func ModelPricingIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldModelPricing))
|
||||
}
|
||||
|
||||
// ModelPricingNotNil applies the NotNil predicate on the "model_pricing" field.
|
||||
func ModelPricingNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldModelPricing))
|
||||
}
|
||||
|
||||
// ClaudeCodeOnlyEQ applies the EQ predicate on the "claude_code_only" field.
|
||||
func ClaudeCodeOnlyEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
|
||||
@@ -2190,6 +2460,96 @@ func MaxReasoningEffortContainsFold(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldContainsFold(FieldMaxReasoningEffort, 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))
|
||||
}
|
||||
|
||||
// ProfitControlEnabledNEQ applies the NEQ predicate on the "profit_control_enabled" field.
|
||||
func ProfitControlEnabledNEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldProfitControlEnabled, v))
|
||||
}
|
||||
|
||||
// ProfitMinMarginEQ applies the EQ predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldProfitMinMargin, v))
|
||||
}
|
||||
|
||||
// ProfitMinMarginNEQ applies the NEQ predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldProfitMinMargin, v))
|
||||
}
|
||||
|
||||
// ProfitMinMarginIn applies the In predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldProfitMinMargin, vs...))
|
||||
}
|
||||
|
||||
// ProfitMinMarginNotIn applies the NotIn predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldProfitMinMargin, vs...))
|
||||
}
|
||||
|
||||
// ProfitMinMarginGT applies the GT predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldProfitMinMargin, v))
|
||||
}
|
||||
|
||||
// ProfitMinMarginGTE applies the GTE predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldProfitMinMargin, v))
|
||||
}
|
||||
|
||||
// ProfitMinMarginLT applies the LT predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldProfitMinMargin, v))
|
||||
}
|
||||
|
||||
// ProfitMinMarginLTE applies the LTE predicate on the "profit_min_margin" field.
|
||||
func ProfitMinMarginLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldProfitMinMargin, v))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferEQ applies the EQ predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldProfitSafetyBuffer, v))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferNEQ applies the NEQ predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldProfitSafetyBuffer, v))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferIn applies the In predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldProfitSafetyBuffer, vs...))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferNotIn applies the NotIn predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldProfitSafetyBuffer, vs...))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferGT applies the GT predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldProfitSafetyBuffer, v))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferGTE applies the GTE predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldProfitSafetyBuffer, v))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferLT applies the LT predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldProfitSafetyBuffer, v))
|
||||
}
|
||||
|
||||
// ProfitSafetyBufferLTE applies the LTE predicate on the "profit_safety_buffer" field.
|
||||
func ProfitSafetyBufferLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldProfitSafetyBuffer, v))
|
||||
}
|
||||
|
||||
// HasAPIKeys applies the HasEdge predicate on the "api_keys" edge.
|
||||
func HasAPIKeys() predicate.Group {
|
||||
return predicate.Group(func(s *sql.Selector) {
|
||||
|
||||
@@ -4,6 +4,7 @@ package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json/jsontext"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -483,6 +484,12 @@ func (_c *GroupCreate) SetNillableVideoPrice1080p(v *float64) *GroupCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetVideoModelPrices sets the "video_model_prices" field.
|
||||
func (_c *GroupCreate) SetVideoModelPrices(v map[string]map[string]float64) *GroupCreate {
|
||||
_c.mutation.SetVideoModelPrices(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
|
||||
func (_c *GroupCreate) SetWebSearchPricePerCall(v float64) *GroupCreate {
|
||||
_c.mutation.SetWebSearchPricePerCall(v)
|
||||
@@ -497,6 +504,82 @@ func (_c *GroupCreate) SetNillableWebSearchPricePerCall(v *float64) *GroupCreate
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetSearchPricePer1k sets the "search_price_per_1k" field.
|
||||
func (_c *GroupCreate) SetSearchPricePer1k(v float64) *GroupCreate {
|
||||
_c.mutation.SetSearchPricePer1k(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableSearchPricePer1k sets the "search_price_per_1k" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableSearchPricePer1k(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetSearchPricePer1k(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field.
|
||||
func (_c *GroupCreate) SetAudioRealtimePricePerMin(v float64) *GroupCreate {
|
||||
_c.mutation.SetAudioRealtimePricePerMin(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableAudioRealtimePricePerMin(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetAudioRealtimePricePerMin(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field.
|
||||
func (_c *GroupCreate) SetAudioTtsPricePerMillionChars(v float64) *GroupCreate {
|
||||
_c.mutation.SetAudioTtsPricePerMillionChars(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableAudioTtsPricePerMillionChars(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetAudioTtsPricePerMillionChars(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field.
|
||||
func (_c *GroupCreate) SetAudioSttPricePerHour(v float64) *GroupCreate {
|
||||
_c.mutation.SetAudioSttPricePerHour(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableAudioSttPricePerHour sets the "audio_stt_price_per_hour" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableAudioSttPricePerHour(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetAudioSttPricePerHour(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetLongContextPricingEnabled sets the "long_context_pricing_enabled" field.
|
||||
func (_c *GroupCreate) SetLongContextPricingEnabled(v bool) *GroupCreate {
|
||||
_c.mutation.SetLongContextPricingEnabled(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableLongContextPricingEnabled sets the "long_context_pricing_enabled" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableLongContextPricingEnabled(v *bool) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetLongContextPricingEnabled(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetModelPricing sets the "model_pricing" field.
|
||||
func (_c *GroupCreate) SetModelPricing(v jsontext.Value) *GroupCreate {
|
||||
_c.mutation.SetModelPricing(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||
func (_c *GroupCreate) SetClaudeCodeOnly(v bool) *GroupCreate {
|
||||
_c.mutation.SetClaudeCodeOnly(v)
|
||||
@@ -725,6 +808,48 @@ func (_c *GroupCreate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapp
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetProfitControlEnabled sets the "profit_control_enabled" field.
|
||||
func (_c *GroupCreate) SetProfitControlEnabled(v bool) *GroupCreate {
|
||||
_c.mutation.SetProfitControlEnabled(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableProfitControlEnabled sets the "profit_control_enabled" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableProfitControlEnabled(v *bool) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetProfitControlEnabled(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetProfitMinMargin sets the "profit_min_margin" field.
|
||||
func (_c *GroupCreate) SetProfitMinMargin(v float64) *GroupCreate {
|
||||
_c.mutation.SetProfitMinMargin(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableProfitMinMargin sets the "profit_min_margin" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableProfitMinMargin(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetProfitMinMargin(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetProfitSafetyBuffer sets the "profit_safety_buffer" field.
|
||||
func (_c *GroupCreate) SetProfitSafetyBuffer(v float64) *GroupCreate {
|
||||
_c.mutation.SetProfitSafetyBuffer(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableProfitSafetyBuffer sets the "profit_safety_buffer" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableProfitSafetyBuffer(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetProfitSafetyBuffer(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
|
||||
func (_c *GroupCreate) AddAPIKeyIDs(ids ...int64) *GroupCreate {
|
||||
_c.mutation.AddAPIKeyIDs(ids...)
|
||||
@@ -938,6 +1063,10 @@ func (_c *GroupCreate) defaults() error {
|
||||
v := group.DefaultVideoRateMultiplier
|
||||
_c.mutation.SetVideoRateMultiplier(v)
|
||||
}
|
||||
if _, ok := _c.mutation.LongContextPricingEnabled(); !ok {
|
||||
v := group.DefaultLongContextPricingEnabled
|
||||
_c.mutation.SetLongContextPricingEnabled(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ClaudeCodeOnly(); !ok {
|
||||
v := group.DefaultClaudeCodeOnly
|
||||
_c.mutation.SetClaudeCodeOnly(v)
|
||||
@@ -998,6 +1127,18 @@ func (_c *GroupCreate) defaults() error {
|
||||
v := group.DefaultReasoningEffortMappings
|
||||
_c.mutation.SetReasoningEffortMappings(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ProfitControlEnabled(); !ok {
|
||||
v := group.DefaultProfitControlEnabled
|
||||
_c.mutation.SetProfitControlEnabled(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ProfitMinMargin(); !ok {
|
||||
v := group.DefaultProfitMinMargin
|
||||
_c.mutation.SetProfitMinMargin(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ProfitSafetyBuffer(); !ok {
|
||||
v := group.DefaultProfitSafetyBuffer
|
||||
_c.mutation.SetProfitSafetyBuffer(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1101,6 +1242,29 @@ func (_c *GroupCreate) check() error {
|
||||
if _, ok := _c.mutation.VideoRateMultiplier(); !ok {
|
||||
return &ValidationError{Name: "video_rate_multiplier", err: errors.New(`ent: missing required field "Group.video_rate_multiplier"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.SearchPricePer1k(); ok {
|
||||
if err := group.SearchPricePer1kValidator(v); err != nil {
|
||||
return &ValidationError{Name: "search_price_per_1k", err: fmt.Errorf(`ent: validator failed for field "Group.search_price_per_1k": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _c.mutation.AudioRealtimePricePerMin(); ok {
|
||||
if err := group.AudioRealtimePricePerMinValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_realtime_price_per_min", err: fmt.Errorf(`ent: validator failed for field "Group.audio_realtime_price_per_min": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _c.mutation.AudioTtsPricePerMillionChars(); ok {
|
||||
if err := group.AudioTtsPricePerMillionCharsValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_tts_price_per_million_chars", err: fmt.Errorf(`ent: validator failed for field "Group.audio_tts_price_per_million_chars": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _c.mutation.AudioSttPricePerHour(); ok {
|
||||
if err := group.AudioSttPricePerHourValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_stt_price_per_hour", err: fmt.Errorf(`ent: validator failed for field "Group.audio_stt_price_per_hour": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.LongContextPricingEnabled(); !ok {
|
||||
return &ValidationError{Name: "long_context_pricing_enabled", err: errors.New(`ent: missing required field "Group.long_context_pricing_enabled"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.ClaudeCodeOnly(); !ok {
|
||||
return &ValidationError{Name: "claude_code_only", err: errors.New(`ent: missing required field "Group.claude_code_only"`)}
|
||||
}
|
||||
@@ -1156,6 +1320,15 @@ func (_c *GroupCreate) check() error {
|
||||
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
|
||||
return &ValidationError{Name: "reasoning_effort_mappings", err: errors.New(`ent: missing required field "Group.reasoning_effort_mappings"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.ProfitControlEnabled(); !ok {
|
||||
return &ValidationError{Name: "profit_control_enabled", err: errors.New(`ent: missing required field "Group.profit_control_enabled"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.ProfitMinMargin(); !ok {
|
||||
return &ValidationError{Name: "profit_min_margin", err: errors.New(`ent: missing required field "Group.profit_min_margin"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.ProfitSafetyBuffer(); !ok {
|
||||
return &ValidationError{Name: "profit_safety_buffer", err: errors.New(`ent: missing required field "Group.profit_safety_buffer"`)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1315,10 +1488,38 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldVideoPrice1080p, field.TypeFloat64, value)
|
||||
_node.VideoPrice1080p = &value
|
||||
}
|
||||
if value, ok := _c.mutation.VideoModelPrices(); ok {
|
||||
_spec.SetField(group.FieldVideoModelPrices, field.TypeJSON, value)
|
||||
_node.VideoModelPrices = value
|
||||
}
|
||||
if value, ok := _c.mutation.WebSearchPricePerCall(); ok {
|
||||
_spec.SetField(group.FieldWebSearchPricePerCall, field.TypeFloat64, value)
|
||||
_node.WebSearchPricePerCall = &value
|
||||
}
|
||||
if value, ok := _c.mutation.SearchPricePer1k(); ok {
|
||||
_spec.SetField(group.FieldSearchPricePer1k, field.TypeFloat64, value)
|
||||
_node.SearchPricePer1k = &value
|
||||
}
|
||||
if value, ok := _c.mutation.AudioRealtimePricePerMin(); ok {
|
||||
_spec.SetField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value)
|
||||
_node.AudioRealtimePricePerMin = &value
|
||||
}
|
||||
if value, ok := _c.mutation.AudioTtsPricePerMillionChars(); ok {
|
||||
_spec.SetField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value)
|
||||
_node.AudioTtsPricePerMillionChars = &value
|
||||
}
|
||||
if value, ok := _c.mutation.AudioSttPricePerHour(); ok {
|
||||
_spec.SetField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value)
|
||||
_node.AudioSttPricePerHour = &value
|
||||
}
|
||||
if value, ok := _c.mutation.LongContextPricingEnabled(); ok {
|
||||
_spec.SetField(group.FieldLongContextPricingEnabled, field.TypeBool, value)
|
||||
_node.LongContextPricingEnabled = value
|
||||
}
|
||||
if value, ok := _c.mutation.ModelPricing(); ok {
|
||||
_spec.SetField(group.FieldModelPricing, field.TypeJSON, value)
|
||||
_node.ModelPricing = value
|
||||
}
|
||||
if value, ok := _c.mutation.ClaudeCodeOnly(); ok {
|
||||
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
||||
_node.ClaudeCodeOnly = value
|
||||
@@ -1391,6 +1592,18 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
|
||||
_node.ReasoningEffortMappings = value
|
||||
}
|
||||
if value, ok := _c.mutation.ProfitControlEnabled(); ok {
|
||||
_spec.SetField(group.FieldProfitControlEnabled, field.TypeBool, value)
|
||||
_node.ProfitControlEnabled = value
|
||||
}
|
||||
if value, ok := _c.mutation.ProfitMinMargin(); ok {
|
||||
_spec.SetField(group.FieldProfitMinMargin, field.TypeFloat64, value)
|
||||
_node.ProfitMinMargin = value
|
||||
}
|
||||
if value, ok := _c.mutation.ProfitSafetyBuffer(); ok {
|
||||
_spec.SetField(group.FieldProfitSafetyBuffer, field.TypeFloat64, value)
|
||||
_node.ProfitSafetyBuffer = value
|
||||
}
|
||||
if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -2081,6 +2294,24 @@ func (u *GroupUpsert) ClearVideoPrice1080p() *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetVideoModelPrices sets the "video_model_prices" field.
|
||||
func (u *GroupUpsert) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpsert {
|
||||
u.Set(group.FieldVideoModelPrices, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateVideoModelPrices sets the "video_model_prices" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateVideoModelPrices() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldVideoModelPrices)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
|
||||
func (u *GroupUpsert) ClearVideoModelPrices() *GroupUpsert {
|
||||
u.SetNull(group.FieldVideoModelPrices)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
|
||||
func (u *GroupUpsert) SetWebSearchPricePerCall(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldWebSearchPricePerCall, v)
|
||||
@@ -2105,6 +2336,132 @@ func (u *GroupUpsert) ClearWebSearchPricePerCall() *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetSearchPricePer1k sets the "search_price_per_1k" field.
|
||||
func (u *GroupUpsert) SetSearchPricePer1k(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldSearchPricePer1k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateSearchPricePer1k sets the "search_price_per_1k" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateSearchPricePer1k() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldSearchPricePer1k)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddSearchPricePer1k adds v to the "search_price_per_1k" field.
|
||||
func (u *GroupUpsert) AddSearchPricePer1k(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldSearchPricePer1k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearSearchPricePer1k clears the value of the "search_price_per_1k" field.
|
||||
func (u *GroupUpsert) ClearSearchPricePer1k() *GroupUpsert {
|
||||
u.SetNull(group.FieldSearchPricePer1k)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsert) SetAudioRealtimePricePerMin(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldAudioRealtimePricePerMin, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateAudioRealtimePricePerMin() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldAudioRealtimePricePerMin)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddAudioRealtimePricePerMin adds v to the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsert) AddAudioRealtimePricePerMin(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldAudioRealtimePricePerMin, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsert) ClearAudioRealtimePricePerMin() *GroupUpsert {
|
||||
u.SetNull(group.FieldAudioRealtimePricePerMin)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsert) SetAudioTtsPricePerMillionChars(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldAudioTtsPricePerMillionChars, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateAudioTtsPricePerMillionChars() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldAudioTtsPricePerMillionChars)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddAudioTtsPricePerMillionChars adds v to the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsert) AddAudioTtsPricePerMillionChars(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldAudioTtsPricePerMillionChars, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsert) ClearAudioTtsPricePerMillionChars() *GroupUpsert {
|
||||
u.SetNull(group.FieldAudioTtsPricePerMillionChars)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsert) SetAudioSttPricePerHour(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldAudioSttPricePerHour, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateAudioSttPricePerHour sets the "audio_stt_price_per_hour" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateAudioSttPricePerHour() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldAudioSttPricePerHour)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddAudioSttPricePerHour adds v to the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsert) AddAudioSttPricePerHour(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldAudioSttPricePerHour, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsert) ClearAudioSttPricePerHour() *GroupUpsert {
|
||||
u.SetNull(group.FieldAudioSttPricePerHour)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetLongContextPricingEnabled sets the "long_context_pricing_enabled" field.
|
||||
func (u *GroupUpsert) SetLongContextPricingEnabled(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldLongContextPricingEnabled, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateLongContextPricingEnabled sets the "long_context_pricing_enabled" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateLongContextPricingEnabled() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldLongContextPricingEnabled)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetModelPricing sets the "model_pricing" field.
|
||||
func (u *GroupUpsert) SetModelPricing(v jsontext.Value) *GroupUpsert {
|
||||
u.Set(group.FieldModelPricing, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateModelPricing sets the "model_pricing" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateModelPricing() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldModelPricing)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearModelPricing clears the value of the "model_pricing" field.
|
||||
func (u *GroupUpsert) ClearModelPricing() *GroupUpsert {
|
||||
u.SetNull(group.FieldModelPricing)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||
func (u *GroupUpsert) SetClaudeCodeOnly(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldClaudeCodeOnly, v)
|
||||
@@ -2363,6 +2720,54 @@ func (u *GroupUpsert) UpdateReasoningEffortMappings() *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetProfitControlEnabled sets the "profit_control_enabled" field.
|
||||
func (u *GroupUpsert) SetProfitControlEnabled(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldProfitControlEnabled, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateProfitControlEnabled sets the "profit_control_enabled" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateProfitControlEnabled() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldProfitControlEnabled)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetProfitMinMargin sets the "profit_min_margin" field.
|
||||
func (u *GroupUpsert) SetProfitMinMargin(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldProfitMinMargin, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateProfitMinMargin sets the "profit_min_margin" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateProfitMinMargin() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldProfitMinMargin)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddProfitMinMargin adds v to the "profit_min_margin" field.
|
||||
func (u *GroupUpsert) AddProfitMinMargin(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldProfitMinMargin, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetProfitSafetyBuffer sets the "profit_safety_buffer" field.
|
||||
func (u *GroupUpsert) SetProfitSafetyBuffer(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldProfitSafetyBuffer, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateProfitSafetyBuffer sets the "profit_safety_buffer" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateProfitSafetyBuffer() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldProfitSafetyBuffer)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddProfitSafetyBuffer adds v to the "profit_safety_buffer" field.
|
||||
func (u *GroupUpsert) AddProfitSafetyBuffer(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldProfitSafetyBuffer, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
||||
// Using this option is equivalent to using:
|
||||
//
|
||||
@@ -3034,6 +3439,27 @@ func (u *GroupUpsertOne) ClearVideoPrice1080p() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoModelPrices sets the "video_model_prices" field.
|
||||
func (u *GroupUpsertOne) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetVideoModelPrices(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoModelPrices sets the "video_model_prices" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateVideoModelPrices() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateVideoModelPrices()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
|
||||
func (u *GroupUpsertOne) ClearVideoModelPrices() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearVideoModelPrices()
|
||||
})
|
||||
}
|
||||
|
||||
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
|
||||
func (u *GroupUpsertOne) SetWebSearchPricePerCall(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -3062,6 +3488,153 @@ func (u *GroupUpsertOne) ClearWebSearchPricePerCall() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetSearchPricePer1k sets the "search_price_per_1k" field.
|
||||
func (u *GroupUpsertOne) SetSearchPricePer1k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetSearchPricePer1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddSearchPricePer1k adds v to the "search_price_per_1k" field.
|
||||
func (u *GroupUpsertOne) AddSearchPricePer1k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddSearchPricePer1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSearchPricePer1k sets the "search_price_per_1k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateSearchPricePer1k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateSearchPricePer1k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearSearchPricePer1k clears the value of the "search_price_per_1k" field.
|
||||
func (u *GroupUpsertOne) ClearSearchPricePer1k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearSearchPricePer1k()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsertOne) SetAudioRealtimePricePerMin(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAudioRealtimePricePerMin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAudioRealtimePricePerMin adds v to the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsertOne) AddAudioRealtimePricePerMin(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddAudioRealtimePricePerMin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateAudioRealtimePricePerMin() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAudioRealtimePricePerMin()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsertOne) ClearAudioRealtimePricePerMin() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearAudioRealtimePricePerMin()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsertOne) SetAudioTtsPricePerMillionChars(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAudioTtsPricePerMillionChars(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAudioTtsPricePerMillionChars adds v to the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsertOne) AddAudioTtsPricePerMillionChars(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddAudioTtsPricePerMillionChars(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateAudioTtsPricePerMillionChars() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAudioTtsPricePerMillionChars()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsertOne) ClearAudioTtsPricePerMillionChars() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearAudioTtsPricePerMillionChars()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsertOne) SetAudioSttPricePerHour(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAudioSttPricePerHour(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAudioSttPricePerHour adds v to the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsertOne) AddAudioSttPricePerHour(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddAudioSttPricePerHour(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAudioSttPricePerHour sets the "audio_stt_price_per_hour" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateAudioSttPricePerHour() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAudioSttPricePerHour()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsertOne) ClearAudioSttPricePerHour() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearAudioSttPricePerHour()
|
||||
})
|
||||
}
|
||||
|
||||
// SetLongContextPricingEnabled sets the "long_context_pricing_enabled" field.
|
||||
func (u *GroupUpsertOne) SetLongContextPricingEnabled(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetLongContextPricingEnabled(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLongContextPricingEnabled sets the "long_context_pricing_enabled" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateLongContextPricingEnabled() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateLongContextPricingEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
// SetModelPricing sets the "model_pricing" field.
|
||||
func (u *GroupUpsertOne) SetModelPricing(v jsontext.Value) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetModelPricing(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateModelPricing sets the "model_pricing" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateModelPricing() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateModelPricing()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearModelPricing clears the value of the "model_pricing" field.
|
||||
func (u *GroupUpsertOne) ClearModelPricing() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearModelPricing()
|
||||
})
|
||||
}
|
||||
|
||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||
func (u *GroupUpsertOne) SetClaudeCodeOnly(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -3363,6 +3936,62 @@ func (u *GroupUpsertOne) UpdateReasoningEffortMappings() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetProfitControlEnabled sets the "profit_control_enabled" field.
|
||||
func (u *GroupUpsertOne) SetProfitControlEnabled(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetProfitControlEnabled(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfitControlEnabled sets the "profit_control_enabled" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateProfitControlEnabled() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateProfitControlEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
// SetProfitMinMargin sets the "profit_min_margin" field.
|
||||
func (u *GroupUpsertOne) SetProfitMinMargin(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetProfitMinMargin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddProfitMinMargin adds v to the "profit_min_margin" field.
|
||||
func (u *GroupUpsertOne) AddProfitMinMargin(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddProfitMinMargin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfitMinMargin sets the "profit_min_margin" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateProfitMinMargin() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateProfitMinMargin()
|
||||
})
|
||||
}
|
||||
|
||||
// SetProfitSafetyBuffer sets the "profit_safety_buffer" field.
|
||||
func (u *GroupUpsertOne) SetProfitSafetyBuffer(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetProfitSafetyBuffer(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddProfitSafetyBuffer adds v to the "profit_safety_buffer" field.
|
||||
func (u *GroupUpsertOne) AddProfitSafetyBuffer(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddProfitSafetyBuffer(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfitSafetyBuffer sets the "profit_safety_buffer" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateProfitSafetyBuffer() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateProfitSafetyBuffer()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *GroupUpsertOne) Exec(ctx context.Context) error {
|
||||
if len(u.create.conflict) == 0 {
|
||||
@@ -4200,6 +4829,27 @@ func (u *GroupUpsertBulk) ClearVideoPrice1080p() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoModelPrices sets the "video_model_prices" field.
|
||||
func (u *GroupUpsertBulk) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetVideoModelPrices(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoModelPrices sets the "video_model_prices" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateVideoModelPrices() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateVideoModelPrices()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
|
||||
func (u *GroupUpsertBulk) ClearVideoModelPrices() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearVideoModelPrices()
|
||||
})
|
||||
}
|
||||
|
||||
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
|
||||
func (u *GroupUpsertBulk) SetWebSearchPricePerCall(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -4228,6 +4878,153 @@ func (u *GroupUpsertBulk) ClearWebSearchPricePerCall() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetSearchPricePer1k sets the "search_price_per_1k" field.
|
||||
func (u *GroupUpsertBulk) SetSearchPricePer1k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetSearchPricePer1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddSearchPricePer1k adds v to the "search_price_per_1k" field.
|
||||
func (u *GroupUpsertBulk) AddSearchPricePer1k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddSearchPricePer1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSearchPricePer1k sets the "search_price_per_1k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateSearchPricePer1k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateSearchPricePer1k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearSearchPricePer1k clears the value of the "search_price_per_1k" field.
|
||||
func (u *GroupUpsertBulk) ClearSearchPricePer1k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearSearchPricePer1k()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsertBulk) SetAudioRealtimePricePerMin(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAudioRealtimePricePerMin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAudioRealtimePricePerMin adds v to the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsertBulk) AddAudioRealtimePricePerMin(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddAudioRealtimePricePerMin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateAudioRealtimePricePerMin() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAudioRealtimePricePerMin()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field.
|
||||
func (u *GroupUpsertBulk) ClearAudioRealtimePricePerMin() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearAudioRealtimePricePerMin()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsertBulk) SetAudioTtsPricePerMillionChars(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAudioTtsPricePerMillionChars(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAudioTtsPricePerMillionChars adds v to the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsertBulk) AddAudioTtsPricePerMillionChars(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddAudioTtsPricePerMillionChars(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateAudioTtsPricePerMillionChars() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAudioTtsPricePerMillionChars()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field.
|
||||
func (u *GroupUpsertBulk) ClearAudioTtsPricePerMillionChars() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearAudioTtsPricePerMillionChars()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsertBulk) SetAudioSttPricePerHour(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAudioSttPricePerHour(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddAudioSttPricePerHour adds v to the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsertBulk) AddAudioSttPricePerHour(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddAudioSttPricePerHour(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAudioSttPricePerHour sets the "audio_stt_price_per_hour" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateAudioSttPricePerHour() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAudioSttPricePerHour()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field.
|
||||
func (u *GroupUpsertBulk) ClearAudioSttPricePerHour() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearAudioSttPricePerHour()
|
||||
})
|
||||
}
|
||||
|
||||
// SetLongContextPricingEnabled sets the "long_context_pricing_enabled" field.
|
||||
func (u *GroupUpsertBulk) SetLongContextPricingEnabled(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetLongContextPricingEnabled(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLongContextPricingEnabled sets the "long_context_pricing_enabled" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateLongContextPricingEnabled() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateLongContextPricingEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
// SetModelPricing sets the "model_pricing" field.
|
||||
func (u *GroupUpsertBulk) SetModelPricing(v jsontext.Value) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetModelPricing(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateModelPricing sets the "model_pricing" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateModelPricing() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateModelPricing()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearModelPricing clears the value of the "model_pricing" field.
|
||||
func (u *GroupUpsertBulk) ClearModelPricing() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearModelPricing()
|
||||
})
|
||||
}
|
||||
|
||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||
func (u *GroupUpsertBulk) SetClaudeCodeOnly(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -4529,6 +5326,62 @@ func (u *GroupUpsertBulk) UpdateReasoningEffortMappings() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetProfitControlEnabled sets the "profit_control_enabled" field.
|
||||
func (u *GroupUpsertBulk) SetProfitControlEnabled(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetProfitControlEnabled(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfitControlEnabled sets the "profit_control_enabled" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateProfitControlEnabled() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateProfitControlEnabled()
|
||||
})
|
||||
}
|
||||
|
||||
// SetProfitMinMargin sets the "profit_min_margin" field.
|
||||
func (u *GroupUpsertBulk) SetProfitMinMargin(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetProfitMinMargin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddProfitMinMargin adds v to the "profit_min_margin" field.
|
||||
func (u *GroupUpsertBulk) AddProfitMinMargin(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddProfitMinMargin(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfitMinMargin sets the "profit_min_margin" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateProfitMinMargin() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateProfitMinMargin()
|
||||
})
|
||||
}
|
||||
|
||||
// SetProfitSafetyBuffer sets the "profit_safety_buffer" field.
|
||||
func (u *GroupUpsertBulk) SetProfitSafetyBuffer(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetProfitSafetyBuffer(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddProfitSafetyBuffer adds v to the "profit_safety_buffer" field.
|
||||
func (u *GroupUpsertBulk) AddProfitSafetyBuffer(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddProfitSafetyBuffer(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfitSafetyBuffer sets the "profit_safety_buffer" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateProfitSafetyBuffer() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateProfitSafetyBuffer()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *GroupUpsertBulk) Exec(ctx context.Context) error {
|
||||
if u.create.err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json/jsontext"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -640,6 +641,18 @@ func (_u *GroupUpdate) ClearVideoPrice1080p() *GroupUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoModelPrices sets the "video_model_prices" field.
|
||||
func (_u *GroupUpdate) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpdate {
|
||||
_u.mutation.SetVideoModelPrices(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
|
||||
func (_u *GroupUpdate) ClearVideoModelPrices() *GroupUpdate {
|
||||
_u.mutation.ClearVideoModelPrices()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
|
||||
func (_u *GroupUpdate) SetWebSearchPricePerCall(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetWebSearchPricePerCall()
|
||||
@@ -667,6 +680,146 @@ func (_u *GroupUpdate) ClearWebSearchPricePerCall() *GroupUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetSearchPricePer1k sets the "search_price_per_1k" field.
|
||||
func (_u *GroupUpdate) SetSearchPricePer1k(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetSearchPricePer1k()
|
||||
_u.mutation.SetSearchPricePer1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableSearchPricePer1k sets the "search_price_per_1k" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableSearchPricePer1k(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetSearchPricePer1k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddSearchPricePer1k adds value to the "search_price_per_1k" field.
|
||||
func (_u *GroupUpdate) AddSearchPricePer1k(v float64) *GroupUpdate {
|
||||
_u.mutation.AddSearchPricePer1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearSearchPricePer1k clears the value of the "search_price_per_1k" field.
|
||||
func (_u *GroupUpdate) ClearSearchPricePer1k() *GroupUpdate {
|
||||
_u.mutation.ClearSearchPricePer1k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field.
|
||||
func (_u *GroupUpdate) SetAudioRealtimePricePerMin(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetAudioRealtimePricePerMin()
|
||||
_u.mutation.SetAudioRealtimePricePerMin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableAudioRealtimePricePerMin(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetAudioRealtimePricePerMin(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAudioRealtimePricePerMin adds value to the "audio_realtime_price_per_min" field.
|
||||
func (_u *GroupUpdate) AddAudioRealtimePricePerMin(v float64) *GroupUpdate {
|
||||
_u.mutation.AddAudioRealtimePricePerMin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field.
|
||||
func (_u *GroupUpdate) ClearAudioRealtimePricePerMin() *GroupUpdate {
|
||||
_u.mutation.ClearAudioRealtimePricePerMin()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field.
|
||||
func (_u *GroupUpdate) SetAudioTtsPricePerMillionChars(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetAudioTtsPricePerMillionChars()
|
||||
_u.mutation.SetAudioTtsPricePerMillionChars(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableAudioTtsPricePerMillionChars(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetAudioTtsPricePerMillionChars(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAudioTtsPricePerMillionChars adds value to the "audio_tts_price_per_million_chars" field.
|
||||
func (_u *GroupUpdate) AddAudioTtsPricePerMillionChars(v float64) *GroupUpdate {
|
||||
_u.mutation.AddAudioTtsPricePerMillionChars(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field.
|
||||
func (_u *GroupUpdate) ClearAudioTtsPricePerMillionChars() *GroupUpdate {
|
||||
_u.mutation.ClearAudioTtsPricePerMillionChars()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field.
|
||||
func (_u *GroupUpdate) SetAudioSttPricePerHour(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetAudioSttPricePerHour()
|
||||
_u.mutation.SetAudioSttPricePerHour(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAudioSttPricePerHour sets the "audio_stt_price_per_hour" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableAudioSttPricePerHour(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetAudioSttPricePerHour(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAudioSttPricePerHour adds value to the "audio_stt_price_per_hour" field.
|
||||
func (_u *GroupUpdate) AddAudioSttPricePerHour(v float64) *GroupUpdate {
|
||||
_u.mutation.AddAudioSttPricePerHour(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field.
|
||||
func (_u *GroupUpdate) ClearAudioSttPricePerHour() *GroupUpdate {
|
||||
_u.mutation.ClearAudioSttPricePerHour()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetLongContextPricingEnabled sets the "long_context_pricing_enabled" field.
|
||||
func (_u *GroupUpdate) SetLongContextPricingEnabled(v bool) *GroupUpdate {
|
||||
_u.mutation.SetLongContextPricingEnabled(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableLongContextPricingEnabled sets the "long_context_pricing_enabled" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableLongContextPricingEnabled(v *bool) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetLongContextPricingEnabled(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetModelPricing sets the "model_pricing" field.
|
||||
func (_u *GroupUpdate) SetModelPricing(v jsontext.Value) *GroupUpdate {
|
||||
_u.mutation.SetModelPricing(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AppendModelPricing appends value to the "model_pricing" field.
|
||||
func (_u *GroupUpdate) AppendModelPricing(v jsontext.Value) *GroupUpdate {
|
||||
_u.mutation.AppendModelPricing(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearModelPricing clears the value of the "model_pricing" field.
|
||||
func (_u *GroupUpdate) ClearModelPricing() *GroupUpdate {
|
||||
_u.mutation.ClearModelPricing()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||
func (_u *GroupUpdate) SetClaudeCodeOnly(v bool) *GroupUpdate {
|
||||
_u.mutation.SetClaudeCodeOnly(v)
|
||||
@@ -953,6 +1106,62 @@ func (_u *GroupUpdate) AppendReasoningEffortMappings(v []domain.ReasoningEffortM
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetProfitControlEnabled sets the "profit_control_enabled" field.
|
||||
func (_u *GroupUpdate) SetProfitControlEnabled(v bool) *GroupUpdate {
|
||||
_u.mutation.SetProfitControlEnabled(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableProfitControlEnabled sets the "profit_control_enabled" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableProfitControlEnabled(v *bool) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetProfitControlEnabled(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetProfitMinMargin sets the "profit_min_margin" field.
|
||||
func (_u *GroupUpdate) SetProfitMinMargin(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetProfitMinMargin()
|
||||
_u.mutation.SetProfitMinMargin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableProfitMinMargin sets the "profit_min_margin" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableProfitMinMargin(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetProfitMinMargin(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddProfitMinMargin adds value to the "profit_min_margin" field.
|
||||
func (_u *GroupUpdate) AddProfitMinMargin(v float64) *GroupUpdate {
|
||||
_u.mutation.AddProfitMinMargin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetProfitSafetyBuffer sets the "profit_safety_buffer" field.
|
||||
func (_u *GroupUpdate) SetProfitSafetyBuffer(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetProfitSafetyBuffer()
|
||||
_u.mutation.SetProfitSafetyBuffer(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableProfitSafetyBuffer sets the "profit_safety_buffer" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableProfitSafetyBuffer(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetProfitSafetyBuffer(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddProfitSafetyBuffer adds value to the "profit_safety_buffer" field.
|
||||
func (_u *GroupUpdate) AddProfitSafetyBuffer(v float64) *GroupUpdate {
|
||||
_u.mutation.AddProfitSafetyBuffer(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
|
||||
func (_u *GroupUpdate) AddAPIKeyIDs(ids ...int64) *GroupUpdate {
|
||||
_u.mutation.AddAPIKeyIDs(ids...)
|
||||
@@ -1248,6 +1457,26 @@ func (_u *GroupUpdate) check() error {
|
||||
return &ValidationError{Name: "subscription_type", err: fmt.Errorf(`ent: validator failed for field "Group.subscription_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.SearchPricePer1k(); ok {
|
||||
if err := group.SearchPricePer1kValidator(v); err != nil {
|
||||
return &ValidationError{Name: "search_price_per_1k", err: fmt.Errorf(`ent: validator failed for field "Group.search_price_per_1k": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.AudioRealtimePricePerMin(); ok {
|
||||
if err := group.AudioRealtimePricePerMinValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_realtime_price_per_min", err: fmt.Errorf(`ent: validator failed for field "Group.audio_realtime_price_per_min": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok {
|
||||
if err := group.AudioTtsPricePerMillionCharsValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_tts_price_per_million_chars", err: fmt.Errorf(`ent: validator failed for field "Group.audio_tts_price_per_million_chars": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.AudioSttPricePerHour(); ok {
|
||||
if err := group.AudioSttPricePerHourValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_stt_price_per_hour", err: fmt.Errorf(`ent: validator failed for field "Group.audio_stt_price_per_hour": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.DefaultMappedModel(); ok {
|
||||
if err := group.DefaultMappedModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "default_mapped_model", err: fmt.Errorf(`ent: validator failed for field "Group.default_mapped_model": %w`, err)}
|
||||
@@ -1450,6 +1679,12 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.VideoPrice1080pCleared() {
|
||||
_spec.ClearField(group.FieldVideoPrice1080p, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoModelPrices(); ok {
|
||||
_spec.SetField(group.FieldVideoModelPrices, field.TypeJSON, value)
|
||||
}
|
||||
if _u.mutation.VideoModelPricesCleared() {
|
||||
_spec.ClearField(group.FieldVideoModelPrices, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.WebSearchPricePerCall(); ok {
|
||||
_spec.SetField(group.FieldWebSearchPricePerCall, field.TypeFloat64, value)
|
||||
}
|
||||
@@ -1459,6 +1694,56 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.WebSearchPricePerCallCleared() {
|
||||
_spec.ClearField(group.FieldWebSearchPricePerCall, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.SearchPricePer1k(); ok {
|
||||
_spec.SetField(group.FieldSearchPricePer1k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedSearchPricePer1k(); ok {
|
||||
_spec.AddField(group.FieldSearchPricePer1k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.SearchPricePer1kCleared() {
|
||||
_spec.ClearField(group.FieldSearchPricePer1k, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.AudioRealtimePricePerMin(); ok {
|
||||
_spec.SetField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAudioRealtimePricePerMin(); ok {
|
||||
_spec.AddField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.AudioRealtimePricePerMinCleared() {
|
||||
_spec.ClearField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok {
|
||||
_spec.SetField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAudioTtsPricePerMillionChars(); ok {
|
||||
_spec.AddField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.AudioTtsPricePerMillionCharsCleared() {
|
||||
_spec.ClearField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.AudioSttPricePerHour(); ok {
|
||||
_spec.SetField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAudioSttPricePerHour(); ok {
|
||||
_spec.AddField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.AudioSttPricePerHourCleared() {
|
||||
_spec.ClearField(group.FieldAudioSttPricePerHour, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.LongContextPricingEnabled(); ok {
|
||||
_spec.SetField(group.FieldLongContextPricingEnabled, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ModelPricing(); ok {
|
||||
_spec.SetField(group.FieldModelPricing, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AppendedModelPricing(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, group.FieldModelPricing, value)
|
||||
})
|
||||
}
|
||||
if _u.mutation.ModelPricingCleared() {
|
||||
_spec.ClearField(group.FieldModelPricing, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
|
||||
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
||||
}
|
||||
@@ -1544,6 +1829,21 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
sqljson.Append(u, group.FieldReasoningEffortMappings, value)
|
||||
})
|
||||
}
|
||||
if value, ok := _u.mutation.ProfitControlEnabled(); ok {
|
||||
_spec.SetField(group.FieldProfitControlEnabled, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ProfitMinMargin(); ok {
|
||||
_spec.SetField(group.FieldProfitMinMargin, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedProfitMinMargin(); ok {
|
||||
_spec.AddField(group.FieldProfitMinMargin, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ProfitSafetyBuffer(); ok {
|
||||
_spec.SetField(group.FieldProfitSafetyBuffer, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedProfitSafetyBuffer(); ok {
|
||||
_spec.AddField(group.FieldProfitSafetyBuffer, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.APIKeysCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -2462,6 +2762,18 @@ func (_u *GroupUpdateOne) ClearVideoPrice1080p() *GroupUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoModelPrices sets the "video_model_prices" field.
|
||||
func (_u *GroupUpdateOne) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpdateOne {
|
||||
_u.mutation.SetVideoModelPrices(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
|
||||
func (_u *GroupUpdateOne) ClearVideoModelPrices() *GroupUpdateOne {
|
||||
_u.mutation.ClearVideoModelPrices()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
|
||||
func (_u *GroupUpdateOne) SetWebSearchPricePerCall(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetWebSearchPricePerCall()
|
||||
@@ -2489,6 +2801,146 @@ func (_u *GroupUpdateOne) ClearWebSearchPricePerCall() *GroupUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetSearchPricePer1k sets the "search_price_per_1k" field.
|
||||
func (_u *GroupUpdateOne) SetSearchPricePer1k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetSearchPricePer1k()
|
||||
_u.mutation.SetSearchPricePer1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableSearchPricePer1k sets the "search_price_per_1k" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableSearchPricePer1k(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetSearchPricePer1k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddSearchPricePer1k adds value to the "search_price_per_1k" field.
|
||||
func (_u *GroupUpdateOne) AddSearchPricePer1k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddSearchPricePer1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearSearchPricePer1k clears the value of the "search_price_per_1k" field.
|
||||
func (_u *GroupUpdateOne) ClearSearchPricePer1k() *GroupUpdateOne {
|
||||
_u.mutation.ClearSearchPricePer1k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field.
|
||||
func (_u *GroupUpdateOne) SetAudioRealtimePricePerMin(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetAudioRealtimePricePerMin()
|
||||
_u.mutation.SetAudioRealtimePricePerMin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableAudioRealtimePricePerMin(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetAudioRealtimePricePerMin(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAudioRealtimePricePerMin adds value to the "audio_realtime_price_per_min" field.
|
||||
func (_u *GroupUpdateOne) AddAudioRealtimePricePerMin(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddAudioRealtimePricePerMin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field.
|
||||
func (_u *GroupUpdateOne) ClearAudioRealtimePricePerMin() *GroupUpdateOne {
|
||||
_u.mutation.ClearAudioRealtimePricePerMin()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field.
|
||||
func (_u *GroupUpdateOne) SetAudioTtsPricePerMillionChars(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetAudioTtsPricePerMillionChars()
|
||||
_u.mutation.SetAudioTtsPricePerMillionChars(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableAudioTtsPricePerMillionChars(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetAudioTtsPricePerMillionChars(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAudioTtsPricePerMillionChars adds value to the "audio_tts_price_per_million_chars" field.
|
||||
func (_u *GroupUpdateOne) AddAudioTtsPricePerMillionChars(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddAudioTtsPricePerMillionChars(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field.
|
||||
func (_u *GroupUpdateOne) ClearAudioTtsPricePerMillionChars() *GroupUpdateOne {
|
||||
_u.mutation.ClearAudioTtsPricePerMillionChars()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field.
|
||||
func (_u *GroupUpdateOne) SetAudioSttPricePerHour(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetAudioSttPricePerHour()
|
||||
_u.mutation.SetAudioSttPricePerHour(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAudioSttPricePerHour sets the "audio_stt_price_per_hour" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableAudioSttPricePerHour(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetAudioSttPricePerHour(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAudioSttPricePerHour adds value to the "audio_stt_price_per_hour" field.
|
||||
func (_u *GroupUpdateOne) AddAudioSttPricePerHour(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddAudioSttPricePerHour(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field.
|
||||
func (_u *GroupUpdateOne) ClearAudioSttPricePerHour() *GroupUpdateOne {
|
||||
_u.mutation.ClearAudioSttPricePerHour()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetLongContextPricingEnabled sets the "long_context_pricing_enabled" field.
|
||||
func (_u *GroupUpdateOne) SetLongContextPricingEnabled(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetLongContextPricingEnabled(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableLongContextPricingEnabled sets the "long_context_pricing_enabled" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableLongContextPricingEnabled(v *bool) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetLongContextPricingEnabled(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetModelPricing sets the "model_pricing" field.
|
||||
func (_u *GroupUpdateOne) SetModelPricing(v jsontext.Value) *GroupUpdateOne {
|
||||
_u.mutation.SetModelPricing(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AppendModelPricing appends value to the "model_pricing" field.
|
||||
func (_u *GroupUpdateOne) AppendModelPricing(v jsontext.Value) *GroupUpdateOne {
|
||||
_u.mutation.AppendModelPricing(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearModelPricing clears the value of the "model_pricing" field.
|
||||
func (_u *GroupUpdateOne) ClearModelPricing() *GroupUpdateOne {
|
||||
_u.mutation.ClearModelPricing()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||
func (_u *GroupUpdateOne) SetClaudeCodeOnly(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetClaudeCodeOnly(v)
|
||||
@@ -2775,6 +3227,62 @@ func (_u *GroupUpdateOne) AppendReasoningEffortMappings(v []domain.ReasoningEffo
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetProfitControlEnabled sets the "profit_control_enabled" field.
|
||||
func (_u *GroupUpdateOne) SetProfitControlEnabled(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetProfitControlEnabled(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableProfitControlEnabled sets the "profit_control_enabled" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableProfitControlEnabled(v *bool) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetProfitControlEnabled(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetProfitMinMargin sets the "profit_min_margin" field.
|
||||
func (_u *GroupUpdateOne) SetProfitMinMargin(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetProfitMinMargin()
|
||||
_u.mutation.SetProfitMinMargin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableProfitMinMargin sets the "profit_min_margin" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableProfitMinMargin(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetProfitMinMargin(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddProfitMinMargin adds value to the "profit_min_margin" field.
|
||||
func (_u *GroupUpdateOne) AddProfitMinMargin(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddProfitMinMargin(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetProfitSafetyBuffer sets the "profit_safety_buffer" field.
|
||||
func (_u *GroupUpdateOne) SetProfitSafetyBuffer(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetProfitSafetyBuffer()
|
||||
_u.mutation.SetProfitSafetyBuffer(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableProfitSafetyBuffer sets the "profit_safety_buffer" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableProfitSafetyBuffer(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetProfitSafetyBuffer(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddProfitSafetyBuffer adds value to the "profit_safety_buffer" field.
|
||||
func (_u *GroupUpdateOne) AddProfitSafetyBuffer(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddProfitSafetyBuffer(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
|
||||
func (_u *GroupUpdateOne) AddAPIKeyIDs(ids ...int64) *GroupUpdateOne {
|
||||
_u.mutation.AddAPIKeyIDs(ids...)
|
||||
@@ -3083,6 +3591,26 @@ func (_u *GroupUpdateOne) check() error {
|
||||
return &ValidationError{Name: "subscription_type", err: fmt.Errorf(`ent: validator failed for field "Group.subscription_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.SearchPricePer1k(); ok {
|
||||
if err := group.SearchPricePer1kValidator(v); err != nil {
|
||||
return &ValidationError{Name: "search_price_per_1k", err: fmt.Errorf(`ent: validator failed for field "Group.search_price_per_1k": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.AudioRealtimePricePerMin(); ok {
|
||||
if err := group.AudioRealtimePricePerMinValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_realtime_price_per_min", err: fmt.Errorf(`ent: validator failed for field "Group.audio_realtime_price_per_min": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok {
|
||||
if err := group.AudioTtsPricePerMillionCharsValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_tts_price_per_million_chars", err: fmt.Errorf(`ent: validator failed for field "Group.audio_tts_price_per_million_chars": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.AudioSttPricePerHour(); ok {
|
||||
if err := group.AudioSttPricePerHourValidator(v); err != nil {
|
||||
return &ValidationError{Name: "audio_stt_price_per_hour", err: fmt.Errorf(`ent: validator failed for field "Group.audio_stt_price_per_hour": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.DefaultMappedModel(); ok {
|
||||
if err := group.DefaultMappedModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "default_mapped_model", err: fmt.Errorf(`ent: validator failed for field "Group.default_mapped_model": %w`, err)}
|
||||
@@ -3302,6 +3830,12 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if _u.mutation.VideoPrice1080pCleared() {
|
||||
_spec.ClearField(group.FieldVideoPrice1080p, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoModelPrices(); ok {
|
||||
_spec.SetField(group.FieldVideoModelPrices, field.TypeJSON, value)
|
||||
}
|
||||
if _u.mutation.VideoModelPricesCleared() {
|
||||
_spec.ClearField(group.FieldVideoModelPrices, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.WebSearchPricePerCall(); ok {
|
||||
_spec.SetField(group.FieldWebSearchPricePerCall, field.TypeFloat64, value)
|
||||
}
|
||||
@@ -3311,6 +3845,56 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if _u.mutation.WebSearchPricePerCallCleared() {
|
||||
_spec.ClearField(group.FieldWebSearchPricePerCall, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.SearchPricePer1k(); ok {
|
||||
_spec.SetField(group.FieldSearchPricePer1k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedSearchPricePer1k(); ok {
|
||||
_spec.AddField(group.FieldSearchPricePer1k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.SearchPricePer1kCleared() {
|
||||
_spec.ClearField(group.FieldSearchPricePer1k, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.AudioRealtimePricePerMin(); ok {
|
||||
_spec.SetField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAudioRealtimePricePerMin(); ok {
|
||||
_spec.AddField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.AudioRealtimePricePerMinCleared() {
|
||||
_spec.ClearField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok {
|
||||
_spec.SetField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAudioTtsPricePerMillionChars(); ok {
|
||||
_spec.AddField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.AudioTtsPricePerMillionCharsCleared() {
|
||||
_spec.ClearField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.AudioSttPricePerHour(); ok {
|
||||
_spec.SetField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedAudioSttPricePerHour(); ok {
|
||||
_spec.AddField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.AudioSttPricePerHourCleared() {
|
||||
_spec.ClearField(group.FieldAudioSttPricePerHour, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.LongContextPricingEnabled(); ok {
|
||||
_spec.SetField(group.FieldLongContextPricingEnabled, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ModelPricing(); ok {
|
||||
_spec.SetField(group.FieldModelPricing, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AppendedModelPricing(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, group.FieldModelPricing, value)
|
||||
})
|
||||
}
|
||||
if _u.mutation.ModelPricingCleared() {
|
||||
_spec.ClearField(group.FieldModelPricing, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
|
||||
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
||||
}
|
||||
@@ -3396,6 +3980,21 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
sqljson.Append(u, group.FieldReasoningEffortMappings, value)
|
||||
})
|
||||
}
|
||||
if value, ok := _u.mutation.ProfitControlEnabled(); ok {
|
||||
_spec.SetField(group.FieldProfitControlEnabled, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ProfitMinMargin(); ok {
|
||||
_spec.SetField(group.FieldProfitMinMargin, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedProfitMinMargin(); ok {
|
||||
_spec.AddField(group.FieldProfitMinMargin, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ProfitSafetyBuffer(); ok {
|
||||
_spec.SetField(group.FieldProfitSafetyBuffer, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedProfitSafetyBuffer(); ok {
|
||||
_spec.AddField(group.FieldProfitSafetyBuffer, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.APIKeysCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
||||
@@ -623,7 +623,9 @@ 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"}},
|
||||
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek"}},
|
||||
{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"},
|
||||
{Name: "endpoint", Type: field.TypeString, Size: 500},
|
||||
{Name: "api_key_encrypted", Type: field.TypeString},
|
||||
@@ -648,7 +650,7 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "channel_monitors_channel_monitor_request_templates_request_template",
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[19]},
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[21]},
|
||||
RefColumns: []*schema.Column{ChannelMonitorRequestTemplatesColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
@@ -657,7 +659,7 @@ var (
|
||||
{
|
||||
Name: "channelmonitor_enabled_last_checked_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[11], ChannelMonitorsColumns[14]},
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[13], ChannelMonitorsColumns[16]},
|
||||
},
|
||||
{
|
||||
Name: "channelmonitor_provider",
|
||||
@@ -667,17 +669,22 @@ var (
|
||||
{
|
||||
Name: "channelmonitor_provider_api_mode",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[4], ChannelMonitorsColumns[5]},
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[4], ChannelMonitorsColumns[7]},
|
||||
},
|
||||
{
|
||||
Name: "channelmonitor_group_name",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[10]},
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[12]},
|
||||
},
|
||||
{
|
||||
Name: "channelmonitor_template_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[19]},
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[21]},
|
||||
},
|
||||
{
|
||||
Name: "channelmonitor_account_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ChannelMonitorsColumns[6]},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -733,6 +740,7 @@ var (
|
||||
{Name: "latency_ms", Type: field.TypeInt, Nullable: true},
|
||||
{Name: "ping_latency_ms", Type: field.TypeInt, Nullable: true},
|
||||
{Name: "message", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
|
||||
{Name: "quota", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "checked_at", Type: field.TypeTime},
|
||||
{Name: "monitor_id", Type: field.TypeInt64},
|
||||
}
|
||||
@@ -744,7 +752,7 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "channel_monitor_histories_channel_monitors_history",
|
||||
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[7]},
|
||||
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[8]},
|
||||
RefColumns: []*schema.Column{ChannelMonitorsColumns[0]},
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
@@ -753,12 +761,12 @@ var (
|
||||
{
|
||||
Name: "channelmonitorhistory_monitor_id_model_checked_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[7], ChannelMonitorHistoriesColumns[1], ChannelMonitorHistoriesColumns[6]},
|
||||
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[8], ChannelMonitorHistoriesColumns[1], ChannelMonitorHistoriesColumns[7]},
|
||||
},
|
||||
{
|
||||
Name: "channelmonitorhistory_checked_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[6]},
|
||||
Columns: []*schema.Column{ChannelMonitorHistoriesColumns[7]},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -768,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"}},
|
||||
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "grok", "antigravity", "kimi", "zhipu", "deepseek"}},
|
||||
{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},
|
||||
@@ -928,7 +936,14 @@ var (
|
||||
{Name: "video_price_480p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "video_price_720p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "video_price_1080p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "video_model_prices", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "web_search_price_per_call", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "search_price_per_1k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "audio_realtime_price_per_min", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "audio_tts_price_per_million_chars", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "audio_stt_price_per_hour", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "long_context_pricing_enabled", Type: field.TypeBool, Default: true},
|
||||
{Name: "model_pricing", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "claude_code_only", Type: field.TypeBool, Default: false},
|
||||
{Name: "fallback_group_id", Type: field.TypeInt64, Nullable: true},
|
||||
{Name: "fallback_group_id_on_invalid_request", Type: field.TypeInt64, Nullable: true},
|
||||
@@ -947,6 +962,9 @@ var (
|
||||
{Name: "rpm_limit", Type: field.TypeInt, Default: 0},
|
||||
{Name: "max_reasoning_effort", Type: field.TypeString, Size: 20, Default: ""},
|
||||
{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)"}},
|
||||
{Name: "profit_safety_buffer", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||
}
|
||||
// GroupsTable holds the schema information for the "groups" table.
|
||||
GroupsTable = &schema.Table{
|
||||
@@ -982,7 +1000,7 @@ var (
|
||||
{
|
||||
Name: "group_sort_order",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{GroupsColumns[42]},
|
||||
Columns: []*schema.Column{GroupsColumns[49]},
|
||||
},
|
||||
{
|
||||
Name: "idx_groups_duplicate_operation_id_active",
|
||||
@@ -1618,6 +1636,8 @@ var (
|
||||
{Name: "model", Type: field.TypeString, Size: 100},
|
||||
{Name: "requested_model", Type: field.TypeString, Nullable: true, Size: 100},
|
||||
{Name: "upstream_model", Type: field.TypeString, Nullable: true, Size: 100},
|
||||
{Name: "upstream_response_model", Type: field.TypeString, Nullable: true, Size: 200},
|
||||
{Name: "upstream_model_mismatch", Type: field.TypeBool, Nullable: true},
|
||||
{Name: "channel_id", Type: field.TypeInt64, Nullable: true},
|
||||
{Name: "model_mapping_chain", Type: field.TypeString, Nullable: true, Size: 500},
|
||||
{Name: "billing_tier", Type: field.TypeString, Nullable: true, Size: 50},
|
||||
@@ -1668,31 +1688,31 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "usage_logs_api_keys_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_accounts_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
RefColumns: []*schema.Column{AccountsColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_groups_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[45]},
|
||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_users_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[46]},
|
||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[45]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[47]},
|
||||
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
@@ -1701,32 +1721,32 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[46]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_account_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_group_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[45]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_subscription_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[45]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[47]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_model",
|
||||
@@ -1746,17 +1766,17 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[44], UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[46], UsageLogsColumns[42]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[41], UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43], UsageLogsColumns[42]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_group_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[43], UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[45], UsageLogsColumns[42]},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -635,36 +635,28 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
// channelmonitorDescCheckMode is the schema descriptor for check_mode field.
|
||||
channelmonitorDescCheckMode := channelmonitorFields[2].Descriptor()
|
||||
// channelmonitor.DefaultCheckMode holds the default value on creation for the check_mode field.
|
||||
channelmonitor.DefaultCheckMode = channelmonitorDescCheckMode.Default.(string)
|
||||
// channelmonitor.CheckModeValidator is a validator for the "check_mode" field. It is called by the builders before save.
|
||||
channelmonitor.CheckModeValidator = channelmonitorDescCheckMode.Validators[0].(func(string) error)
|
||||
// channelmonitorDescAPIMode is the schema descriptor for api_mode field.
|
||||
channelmonitorDescAPIMode := channelmonitorFields[2].Descriptor()
|
||||
channelmonitorDescAPIMode := channelmonitorFields[4].Descriptor()
|
||||
// channelmonitor.DefaultAPIMode holds the default value on creation for the api_mode field.
|
||||
channelmonitor.DefaultAPIMode = channelmonitorDescAPIMode.Default.(string)
|
||||
// channelmonitor.APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save.
|
||||
channelmonitor.APIModeValidator = channelmonitorDescAPIMode.Validators[0].(func(string) error)
|
||||
// channelmonitorDescEndpoint is the schema descriptor for endpoint field.
|
||||
channelmonitorDescEndpoint := channelmonitorFields[3].Descriptor()
|
||||
channelmonitorDescEndpoint := channelmonitorFields[5].Descriptor()
|
||||
// channelmonitor.EndpointValidator is a validator for the "endpoint" field. It is called by the builders before save.
|
||||
channelmonitor.EndpointValidator = func() func(string) error {
|
||||
validators := channelmonitorDescEndpoint.Validators
|
||||
fns := [...]func(string) error{
|
||||
validators[0].(func(string) error),
|
||||
validators[1].(func(string) error),
|
||||
}
|
||||
return func(endpoint string) error {
|
||||
for _, fn := range fns {
|
||||
if err := fn(endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
channelmonitor.EndpointValidator = channelmonitorDescEndpoint.Validators[0].(func(string) error)
|
||||
// channelmonitorDescAPIKeyEncrypted is the schema descriptor for api_key_encrypted field.
|
||||
channelmonitorDescAPIKeyEncrypted := channelmonitorFields[4].Descriptor()
|
||||
channelmonitorDescAPIKeyEncrypted := channelmonitorFields[6].Descriptor()
|
||||
// channelmonitor.APIKeyEncryptedValidator is a validator for the "api_key_encrypted" field. It is called by the builders before save.
|
||||
channelmonitor.APIKeyEncryptedValidator = channelmonitorDescAPIKeyEncrypted.Validators[0].(func(string) error)
|
||||
// channelmonitorDescPrimaryModel is the schema descriptor for primary_model field.
|
||||
channelmonitorDescPrimaryModel := channelmonitorFields[5].Descriptor()
|
||||
channelmonitorDescPrimaryModel := channelmonitorFields[7].Descriptor()
|
||||
// channelmonitor.PrimaryModelValidator is a validator for the "primary_model" field. It is called by the builders before save.
|
||||
channelmonitor.PrimaryModelValidator = func() func(string) error {
|
||||
validators := channelmonitorDescPrimaryModel.Validators
|
||||
@@ -682,35 +674,35 @@ func init() {
|
||||
}
|
||||
}()
|
||||
// channelmonitorDescExtraModels is the schema descriptor for extra_models field.
|
||||
channelmonitorDescExtraModels := channelmonitorFields[6].Descriptor()
|
||||
channelmonitorDescExtraModels := channelmonitorFields[8].Descriptor()
|
||||
// channelmonitor.DefaultExtraModels holds the default value on creation for the extra_models field.
|
||||
channelmonitor.DefaultExtraModels = channelmonitorDescExtraModels.Default.([]string)
|
||||
// channelmonitorDescGroupName is the schema descriptor for group_name field.
|
||||
channelmonitorDescGroupName := channelmonitorFields[7].Descriptor()
|
||||
channelmonitorDescGroupName := channelmonitorFields[9].Descriptor()
|
||||
// channelmonitor.DefaultGroupName holds the default value on creation for the group_name field.
|
||||
channelmonitor.DefaultGroupName = channelmonitorDescGroupName.Default.(string)
|
||||
// channelmonitor.GroupNameValidator is a validator for the "group_name" field. It is called by the builders before save.
|
||||
channelmonitor.GroupNameValidator = channelmonitorDescGroupName.Validators[0].(func(string) error)
|
||||
// channelmonitorDescEnabled is the schema descriptor for enabled field.
|
||||
channelmonitorDescEnabled := channelmonitorFields[8].Descriptor()
|
||||
channelmonitorDescEnabled := channelmonitorFields[10].Descriptor()
|
||||
// channelmonitor.DefaultEnabled holds the default value on creation for the enabled field.
|
||||
channelmonitor.DefaultEnabled = channelmonitorDescEnabled.Default.(bool)
|
||||
// channelmonitorDescIntervalSeconds is the schema descriptor for interval_seconds field.
|
||||
channelmonitorDescIntervalSeconds := channelmonitorFields[9].Descriptor()
|
||||
channelmonitorDescIntervalSeconds := channelmonitorFields[11].Descriptor()
|
||||
// channelmonitor.IntervalSecondsValidator is a validator for the "interval_seconds" field. It is called by the builders before save.
|
||||
channelmonitor.IntervalSecondsValidator = channelmonitorDescIntervalSeconds.Validators[0].(func(int) error)
|
||||
// channelmonitorDescJitterSeconds is the schema descriptor for jitter_seconds field.
|
||||
channelmonitorDescJitterSeconds := channelmonitorFields[10].Descriptor()
|
||||
channelmonitorDescJitterSeconds := channelmonitorFields[12].Descriptor()
|
||||
// channelmonitor.DefaultJitterSeconds holds the default value on creation for the jitter_seconds field.
|
||||
channelmonitor.DefaultJitterSeconds = channelmonitorDescJitterSeconds.Default.(int)
|
||||
// channelmonitor.JitterSecondsValidator is a validator for the "jitter_seconds" field. It is called by the builders before save.
|
||||
channelmonitor.JitterSecondsValidator = channelmonitorDescJitterSeconds.Validators[0].(func(int) error)
|
||||
// channelmonitorDescExtraHeaders is the schema descriptor for extra_headers field.
|
||||
channelmonitorDescExtraHeaders := channelmonitorFields[14].Descriptor()
|
||||
channelmonitorDescExtraHeaders := channelmonitorFields[16].Descriptor()
|
||||
// channelmonitor.DefaultExtraHeaders holds the default value on creation for the extra_headers field.
|
||||
channelmonitor.DefaultExtraHeaders = channelmonitorDescExtraHeaders.Default.(map[string]string)
|
||||
// channelmonitorDescBodyOverrideMode is the schema descriptor for body_override_mode field.
|
||||
channelmonitorDescBodyOverrideMode := channelmonitorFields[15].Descriptor()
|
||||
channelmonitorDescBodyOverrideMode := channelmonitorFields[17].Descriptor()
|
||||
// channelmonitor.DefaultBodyOverrideMode holds the default value on creation for the body_override_mode field.
|
||||
channelmonitor.DefaultBodyOverrideMode = channelmonitorDescBodyOverrideMode.Default.(string)
|
||||
// channelmonitor.BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save.
|
||||
@@ -808,7 +800,7 @@ func init() {
|
||||
// channelmonitorhistory.MessageValidator is a validator for the "message" field. It is called by the builders before save.
|
||||
channelmonitorhistory.MessageValidator = channelmonitorhistoryDescMessage.Validators[0].(func(string) error)
|
||||
// channelmonitorhistoryDescCheckedAt is the schema descriptor for checked_at field.
|
||||
channelmonitorhistoryDescCheckedAt := channelmonitorhistoryFields[6].Descriptor()
|
||||
channelmonitorhistoryDescCheckedAt := channelmonitorhistoryFields[7].Descriptor()
|
||||
// channelmonitorhistory.DefaultCheckedAt holds the default value on creation for the checked_at field.
|
||||
channelmonitorhistory.DefaultCheckedAt = channelmonitorhistoryDescCheckedAt.Default.(func() time.Time)
|
||||
channelmonitorrequesttemplateMixin := schema.ChannelMonitorRequestTemplate{}.Mixin()
|
||||
@@ -1117,70 +1109,102 @@ func init() {
|
||||
groupDescVideoRateMultiplier := groupFields[26].Descriptor()
|
||||
// group.DefaultVideoRateMultiplier holds the default value on creation for the video_rate_multiplier field.
|
||||
group.DefaultVideoRateMultiplier = groupDescVideoRateMultiplier.Default.(float64)
|
||||
// groupDescSearchPricePer1k is the schema descriptor for search_price_per_1k field.
|
||||
groupDescSearchPricePer1k := groupFields[32].Descriptor()
|
||||
// group.SearchPricePer1kValidator is a validator for the "search_price_per_1k" field. It is called by the builders before save.
|
||||
group.SearchPricePer1kValidator = groupDescSearchPricePer1k.Validators[0].(func(float64) error)
|
||||
// groupDescAudioRealtimePricePerMin is the schema descriptor for audio_realtime_price_per_min field.
|
||||
groupDescAudioRealtimePricePerMin := groupFields[33].Descriptor()
|
||||
// group.AudioRealtimePricePerMinValidator is a validator for the "audio_realtime_price_per_min" field. It is called by the builders before save.
|
||||
group.AudioRealtimePricePerMinValidator = groupDescAudioRealtimePricePerMin.Validators[0].(func(float64) error)
|
||||
// groupDescAudioTtsPricePerMillionChars is the schema descriptor for audio_tts_price_per_million_chars field.
|
||||
groupDescAudioTtsPricePerMillionChars := groupFields[34].Descriptor()
|
||||
// group.AudioTtsPricePerMillionCharsValidator is a validator for the "audio_tts_price_per_million_chars" field. It is called by the builders before save.
|
||||
group.AudioTtsPricePerMillionCharsValidator = groupDescAudioTtsPricePerMillionChars.Validators[0].(func(float64) error)
|
||||
// groupDescAudioSttPricePerHour is the schema descriptor for audio_stt_price_per_hour field.
|
||||
groupDescAudioSttPricePerHour := groupFields[35].Descriptor()
|
||||
// group.AudioSttPricePerHourValidator is a validator for the "audio_stt_price_per_hour" field. It is called by the builders before save.
|
||||
group.AudioSttPricePerHourValidator = groupDescAudioSttPricePerHour.Validators[0].(func(float64) error)
|
||||
// groupDescLongContextPricingEnabled is the schema descriptor for long_context_pricing_enabled field.
|
||||
groupDescLongContextPricingEnabled := groupFields[36].Descriptor()
|
||||
// group.DefaultLongContextPricingEnabled holds the default value on creation for the long_context_pricing_enabled field.
|
||||
group.DefaultLongContextPricingEnabled = groupDescLongContextPricingEnabled.Default.(bool)
|
||||
// groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field.
|
||||
groupDescClaudeCodeOnly := groupFields[31].Descriptor()
|
||||
groupDescClaudeCodeOnly := groupFields[38].Descriptor()
|
||||
// group.DefaultClaudeCodeOnly holds the default value on creation for the claude_code_only field.
|
||||
group.DefaultClaudeCodeOnly = groupDescClaudeCodeOnly.Default.(bool)
|
||||
// groupDescModelRoutingEnabled is the schema descriptor for model_routing_enabled field.
|
||||
groupDescModelRoutingEnabled := groupFields[35].Descriptor()
|
||||
groupDescModelRoutingEnabled := groupFields[42].Descriptor()
|
||||
// group.DefaultModelRoutingEnabled holds the default value on creation for the model_routing_enabled field.
|
||||
group.DefaultModelRoutingEnabled = groupDescModelRoutingEnabled.Default.(bool)
|
||||
// groupDescMcpXMLInject is the schema descriptor for mcp_xml_inject field.
|
||||
groupDescMcpXMLInject := groupFields[36].Descriptor()
|
||||
groupDescMcpXMLInject := groupFields[43].Descriptor()
|
||||
// group.DefaultMcpXMLInject holds the default value on creation for the mcp_xml_inject field.
|
||||
group.DefaultMcpXMLInject = groupDescMcpXMLInject.Default.(bool)
|
||||
// groupDescSupportedModelScopes is the schema descriptor for supported_model_scopes field.
|
||||
groupDescSupportedModelScopes := groupFields[37].Descriptor()
|
||||
groupDescSupportedModelScopes := groupFields[44].Descriptor()
|
||||
// group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field.
|
||||
group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string)
|
||||
// groupDescSortOrder is the schema descriptor for sort_order field.
|
||||
groupDescSortOrder := groupFields[38].Descriptor()
|
||||
groupDescSortOrder := groupFields[45].Descriptor()
|
||||
// group.DefaultSortOrder holds the default value on creation for the sort_order field.
|
||||
group.DefaultSortOrder = groupDescSortOrder.Default.(int)
|
||||
// groupDescAllowMessagesDispatch is the schema descriptor for allow_messages_dispatch field.
|
||||
groupDescAllowMessagesDispatch := groupFields[39].Descriptor()
|
||||
groupDescAllowMessagesDispatch := groupFields[46].Descriptor()
|
||||
// group.DefaultAllowMessagesDispatch holds the default value on creation for the allow_messages_dispatch field.
|
||||
group.DefaultAllowMessagesDispatch = groupDescAllowMessagesDispatch.Default.(bool)
|
||||
// groupDescAllowLive is the schema descriptor for allow_live field.
|
||||
groupDescAllowLive := groupFields[40].Descriptor()
|
||||
groupDescAllowLive := groupFields[47].Descriptor()
|
||||
// group.DefaultAllowLive holds the default value on creation for the allow_live field.
|
||||
group.DefaultAllowLive = groupDescAllowLive.Default.(bool)
|
||||
// groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field.
|
||||
groupDescRequireOauthOnly := groupFields[41].Descriptor()
|
||||
groupDescRequireOauthOnly := groupFields[48].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[42].Descriptor()
|
||||
groupDescRequirePrivacySet := groupFields[49].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[43].Descriptor()
|
||||
groupDescDefaultMappedModel := groupFields[50].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[44].Descriptor()
|
||||
groupDescMessagesDispatchModelConfig := groupFields[51].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[45].Descriptor()
|
||||
groupDescModelsListConfig := groupFields[52].Descriptor()
|
||||
// group.DefaultModelsListConfig holds the default value on creation for the models_list_config field.
|
||||
group.DefaultModelsListConfig = groupDescModelsListConfig.Default.(domain.GroupModelsListConfig)
|
||||
// groupDescRpmLimit is the schema descriptor for rpm_limit field.
|
||||
groupDescRpmLimit := groupFields[46].Descriptor()
|
||||
groupDescRpmLimit := groupFields[53].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[47].Descriptor()
|
||||
groupDescMaxReasoningEffort := groupFields[54].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)
|
||||
// groupDescReasoningEffortMappings is the schema descriptor for reasoning_effort_mappings field.
|
||||
groupDescReasoningEffortMappings := groupFields[48].Descriptor()
|
||||
groupDescReasoningEffortMappings := groupFields[55].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()
|
||||
// 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()
|
||||
// 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()
|
||||
// group.DefaultProfitSafetyBuffer holds the default value on creation for the profit_safety_buffer field.
|
||||
group.DefaultProfitSafetyBuffer = groupDescProfitSafetyBuffer.Default.(float64)
|
||||
idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin()
|
||||
idempotencyrecordMixinFields0 := idempotencyrecordMixin[0].Fields()
|
||||
_ = idempotencyrecordMixinFields0
|
||||
@@ -1970,124 +1994,128 @@ func init() {
|
||||
usagelogDescUpstreamModel := usagelogFields[6].Descriptor()
|
||||
// usagelog.UpstreamModelValidator is a validator for the "upstream_model" field. It is called by the builders before save.
|
||||
usagelog.UpstreamModelValidator = usagelogDescUpstreamModel.Validators[0].(func(string) error)
|
||||
// usagelogDescUpstreamResponseModel is the schema descriptor for upstream_response_model field.
|
||||
usagelogDescUpstreamResponseModel := usagelogFields[7].Descriptor()
|
||||
// usagelog.UpstreamResponseModelValidator is a validator for the "upstream_response_model" field. It is called by the builders before save.
|
||||
usagelog.UpstreamResponseModelValidator = usagelogDescUpstreamResponseModel.Validators[0].(func(string) error)
|
||||
// usagelogDescModelMappingChain is the schema descriptor for model_mapping_chain field.
|
||||
usagelogDescModelMappingChain := usagelogFields[8].Descriptor()
|
||||
usagelogDescModelMappingChain := usagelogFields[10].Descriptor()
|
||||
// usagelog.ModelMappingChainValidator is a validator for the "model_mapping_chain" field. It is called by the builders before save.
|
||||
usagelog.ModelMappingChainValidator = usagelogDescModelMappingChain.Validators[0].(func(string) error)
|
||||
// usagelogDescBillingTier is the schema descriptor for billing_tier field.
|
||||
usagelogDescBillingTier := usagelogFields[9].Descriptor()
|
||||
usagelogDescBillingTier := usagelogFields[11].Descriptor()
|
||||
// usagelog.BillingTierValidator is a validator for the "billing_tier" field. It is called by the builders before save.
|
||||
usagelog.BillingTierValidator = usagelogDescBillingTier.Validators[0].(func(string) error)
|
||||
// usagelogDescBillingMode is the schema descriptor for billing_mode field.
|
||||
usagelogDescBillingMode := usagelogFields[10].Descriptor()
|
||||
usagelogDescBillingMode := usagelogFields[12].Descriptor()
|
||||
// usagelog.BillingModeValidator is a validator for the "billing_mode" field. It is called by the builders before save.
|
||||
usagelog.BillingModeValidator = usagelogDescBillingMode.Validators[0].(func(string) error)
|
||||
// usagelogDescInputTokens is the schema descriptor for input_tokens field.
|
||||
usagelogDescInputTokens := usagelogFields[13].Descriptor()
|
||||
usagelogDescInputTokens := usagelogFields[15].Descriptor()
|
||||
// usagelog.DefaultInputTokens holds the default value on creation for the input_tokens field.
|
||||
usagelog.DefaultInputTokens = usagelogDescInputTokens.Default.(int)
|
||||
// usagelogDescOutputTokens is the schema descriptor for output_tokens field.
|
||||
usagelogDescOutputTokens := usagelogFields[14].Descriptor()
|
||||
usagelogDescOutputTokens := usagelogFields[16].Descriptor()
|
||||
// usagelog.DefaultOutputTokens holds the default value on creation for the output_tokens field.
|
||||
usagelog.DefaultOutputTokens = usagelogDescOutputTokens.Default.(int)
|
||||
// usagelogDescCacheCreationTokens is the schema descriptor for cache_creation_tokens field.
|
||||
usagelogDescCacheCreationTokens := usagelogFields[15].Descriptor()
|
||||
usagelogDescCacheCreationTokens := usagelogFields[17].Descriptor()
|
||||
// usagelog.DefaultCacheCreationTokens holds the default value on creation for the cache_creation_tokens field.
|
||||
usagelog.DefaultCacheCreationTokens = usagelogDescCacheCreationTokens.Default.(int)
|
||||
// usagelogDescCacheReadTokens is the schema descriptor for cache_read_tokens field.
|
||||
usagelogDescCacheReadTokens := usagelogFields[16].Descriptor()
|
||||
usagelogDescCacheReadTokens := usagelogFields[18].Descriptor()
|
||||
// usagelog.DefaultCacheReadTokens holds the default value on creation for the cache_read_tokens field.
|
||||
usagelog.DefaultCacheReadTokens = usagelogDescCacheReadTokens.Default.(int)
|
||||
// usagelogDescCacheCreation5mTokens is the schema descriptor for cache_creation_5m_tokens field.
|
||||
usagelogDescCacheCreation5mTokens := usagelogFields[17].Descriptor()
|
||||
usagelogDescCacheCreation5mTokens := usagelogFields[19].Descriptor()
|
||||
// usagelog.DefaultCacheCreation5mTokens holds the default value on creation for the cache_creation_5m_tokens field.
|
||||
usagelog.DefaultCacheCreation5mTokens = usagelogDescCacheCreation5mTokens.Default.(int)
|
||||
// usagelogDescCacheCreation1hTokens is the schema descriptor for cache_creation_1h_tokens field.
|
||||
usagelogDescCacheCreation1hTokens := usagelogFields[18].Descriptor()
|
||||
usagelogDescCacheCreation1hTokens := usagelogFields[20].Descriptor()
|
||||
// usagelog.DefaultCacheCreation1hTokens holds the default value on creation for the cache_creation_1h_tokens field.
|
||||
usagelog.DefaultCacheCreation1hTokens = usagelogDescCacheCreation1hTokens.Default.(int)
|
||||
// usagelogDescInputCost is the schema descriptor for input_cost field.
|
||||
usagelogDescInputCost := usagelogFields[19].Descriptor()
|
||||
usagelogDescInputCost := usagelogFields[21].Descriptor()
|
||||
// usagelog.DefaultInputCost holds the default value on creation for the input_cost field.
|
||||
usagelog.DefaultInputCost = usagelogDescInputCost.Default.(float64)
|
||||
// usagelogDescOutputCost is the schema descriptor for output_cost field.
|
||||
usagelogDescOutputCost := usagelogFields[20].Descriptor()
|
||||
usagelogDescOutputCost := usagelogFields[22].Descriptor()
|
||||
// usagelog.DefaultOutputCost holds the default value on creation for the output_cost field.
|
||||
usagelog.DefaultOutputCost = usagelogDescOutputCost.Default.(float64)
|
||||
// usagelogDescCacheCreationCost is the schema descriptor for cache_creation_cost field.
|
||||
usagelogDescCacheCreationCost := usagelogFields[21].Descriptor()
|
||||
usagelogDescCacheCreationCost := usagelogFields[23].Descriptor()
|
||||
// usagelog.DefaultCacheCreationCost holds the default value on creation for the cache_creation_cost field.
|
||||
usagelog.DefaultCacheCreationCost = usagelogDescCacheCreationCost.Default.(float64)
|
||||
// usagelogDescCacheReadCost is the schema descriptor for cache_read_cost field.
|
||||
usagelogDescCacheReadCost := usagelogFields[22].Descriptor()
|
||||
usagelogDescCacheReadCost := usagelogFields[24].Descriptor()
|
||||
// usagelog.DefaultCacheReadCost holds the default value on creation for the cache_read_cost field.
|
||||
usagelog.DefaultCacheReadCost = usagelogDescCacheReadCost.Default.(float64)
|
||||
// usagelogDescTotalCost is the schema descriptor for total_cost field.
|
||||
usagelogDescTotalCost := usagelogFields[23].Descriptor()
|
||||
usagelogDescTotalCost := usagelogFields[25].Descriptor()
|
||||
// usagelog.DefaultTotalCost holds the default value on creation for the total_cost field.
|
||||
usagelog.DefaultTotalCost = usagelogDescTotalCost.Default.(float64)
|
||||
// usagelogDescActualCost is the schema descriptor for actual_cost field.
|
||||
usagelogDescActualCost := usagelogFields[24].Descriptor()
|
||||
usagelogDescActualCost := usagelogFields[26].Descriptor()
|
||||
// usagelog.DefaultActualCost holds the default value on creation for the actual_cost field.
|
||||
usagelog.DefaultActualCost = usagelogDescActualCost.Default.(float64)
|
||||
// usagelogDescRateMultiplier is the schema descriptor for rate_multiplier field.
|
||||
usagelogDescRateMultiplier := usagelogFields[25].Descriptor()
|
||||
usagelogDescRateMultiplier := usagelogFields[27].Descriptor()
|
||||
// usagelog.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field.
|
||||
usagelog.DefaultRateMultiplier = usagelogDescRateMultiplier.Default.(float64)
|
||||
// usagelogDescLongContextBillingApplied is the schema descriptor for long_context_billing_applied field.
|
||||
usagelogDescLongContextBillingApplied := usagelogFields[26].Descriptor()
|
||||
usagelogDescLongContextBillingApplied := usagelogFields[28].Descriptor()
|
||||
// usagelog.DefaultLongContextBillingApplied holds the default value on creation for the long_context_billing_applied field.
|
||||
usagelog.DefaultLongContextBillingApplied = usagelogDescLongContextBillingApplied.Default.(bool)
|
||||
// usagelogDescBillingType is the schema descriptor for billing_type field.
|
||||
usagelogDescBillingType := usagelogFields[28].Descriptor()
|
||||
usagelogDescBillingType := usagelogFields[30].Descriptor()
|
||||
// usagelog.DefaultBillingType holds the default value on creation for the billing_type field.
|
||||
usagelog.DefaultBillingType = usagelogDescBillingType.Default.(int8)
|
||||
// usagelogDescStream is the schema descriptor for stream field.
|
||||
usagelogDescStream := usagelogFields[29].Descriptor()
|
||||
usagelogDescStream := usagelogFields[31].Descriptor()
|
||||
// usagelog.DefaultStream holds the default value on creation for the stream field.
|
||||
usagelog.DefaultStream = usagelogDescStream.Default.(bool)
|
||||
// usagelogDescUserAgent is the schema descriptor for user_agent field.
|
||||
usagelogDescUserAgent := usagelogFields[32].Descriptor()
|
||||
usagelogDescUserAgent := usagelogFields[34].Descriptor()
|
||||
// usagelog.UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save.
|
||||
usagelog.UserAgentValidator = usagelogDescUserAgent.Validators[0].(func(string) error)
|
||||
// usagelogDescIPAddress is the schema descriptor for ip_address field.
|
||||
usagelogDescIPAddress := usagelogFields[33].Descriptor()
|
||||
usagelogDescIPAddress := usagelogFields[35].Descriptor()
|
||||
// usagelog.IPAddressValidator is a validator for the "ip_address" field. It is called by the builders before save.
|
||||
usagelog.IPAddressValidator = usagelogDescIPAddress.Validators[0].(func(string) error)
|
||||
// usagelogDescImageCount is the schema descriptor for image_count field.
|
||||
usagelogDescImageCount := usagelogFields[34].Descriptor()
|
||||
usagelogDescImageCount := usagelogFields[36].Descriptor()
|
||||
// usagelog.DefaultImageCount holds the default value on creation for the image_count field.
|
||||
usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int)
|
||||
// usagelogDescImageSize is the schema descriptor for image_size field.
|
||||
usagelogDescImageSize := usagelogFields[35].Descriptor()
|
||||
usagelogDescImageSize := usagelogFields[37].Descriptor()
|
||||
// usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
||||
usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error)
|
||||
// usagelogDescImageInputSize is the schema descriptor for image_input_size field.
|
||||
usagelogDescImageInputSize := usagelogFields[36].Descriptor()
|
||||
usagelogDescImageInputSize := usagelogFields[38].Descriptor()
|
||||
// usagelog.ImageInputSizeValidator is a validator for the "image_input_size" field. It is called by the builders before save.
|
||||
usagelog.ImageInputSizeValidator = usagelogDescImageInputSize.Validators[0].(func(string) error)
|
||||
// usagelogDescImageOutputSize is the schema descriptor for image_output_size field.
|
||||
usagelogDescImageOutputSize := usagelogFields[37].Descriptor()
|
||||
usagelogDescImageOutputSize := usagelogFields[39].Descriptor()
|
||||
// usagelog.ImageOutputSizeValidator is a validator for the "image_output_size" field. It is called by the builders before save.
|
||||
usagelog.ImageOutputSizeValidator = usagelogDescImageOutputSize.Validators[0].(func(string) error)
|
||||
// usagelogDescImageSizeSource is the schema descriptor for image_size_source field.
|
||||
usagelogDescImageSizeSource := usagelogFields[38].Descriptor()
|
||||
usagelogDescImageSizeSource := usagelogFields[40].Descriptor()
|
||||
// usagelog.ImageSizeSourceValidator is a validator for the "image_size_source" field. It is called by the builders before save.
|
||||
usagelog.ImageSizeSourceValidator = usagelogDescImageSizeSource.Validators[0].(func(string) error)
|
||||
// usagelogDescVideoCount is the schema descriptor for video_count field.
|
||||
usagelogDescVideoCount := usagelogFields[40].Descriptor()
|
||||
usagelogDescVideoCount := usagelogFields[42].Descriptor()
|
||||
// usagelog.DefaultVideoCount holds the default value on creation for the video_count field.
|
||||
usagelog.DefaultVideoCount = usagelogDescVideoCount.Default.(int)
|
||||
// usagelogDescVideoResolution is the schema descriptor for video_resolution field.
|
||||
usagelogDescVideoResolution := usagelogFields[41].Descriptor()
|
||||
usagelogDescVideoResolution := usagelogFields[43].Descriptor()
|
||||
// usagelog.VideoResolutionValidator is a validator for the "video_resolution" field. It is called by the builders before save.
|
||||
usagelog.VideoResolutionValidator = usagelogDescVideoResolution.Validators[0].(func(string) error)
|
||||
// usagelogDescCacheTTLOverridden is the schema descriptor for cache_ttl_overridden field.
|
||||
usagelogDescCacheTTLOverridden := usagelogFields[43].Descriptor()
|
||||
usagelogDescCacheTTLOverridden := usagelogFields[45].Descriptor()
|
||||
// usagelog.DefaultCacheTTLOverridden holds the default value on creation for the cache_ttl_overridden field.
|
||||
usagelog.DefaultCacheTTLOverridden = usagelogDescCacheTTLOverridden.Default.(bool)
|
||||
// usagelogDescCreatedAt is the schema descriptor for created_at field.
|
||||
usagelogDescCreatedAt := usagelogFields[44].Descriptor()
|
||||
usagelogDescCreatedAt := usagelogFields[46].Descriptor()
|
||||
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
|
||||
userMixin := schema.User{}.Mixin()
|
||||
|
||||
@@ -35,15 +35,32 @@ func (ChannelMonitor) Fields() []ent.Field {
|
||||
NotEmpty().
|
||||
MaxLen(100),
|
||||
field.Enum("provider").
|
||||
Values("openai", "anthropic", "gemini", "grok"),
|
||||
Values("openai", "anthropic", "gemini", "grok",
|
||||
"antigravity", "kimi", "zhipu", "deepseek"),
|
||||
// check_mode: 'probe' | 'quota' | 'quota_probe'
|
||||
// probe - LLM 探活(默认,原有行为)
|
||||
// quota - 仅查关联账号的用量/余额(零 LLM 成本;endpoint/api_key 可空)
|
||||
// quota_probe - 探活 + 配额并存(配额快照挂到主模型历史行)
|
||||
// antigravity 无探活 adapter,仅允许 quota。
|
||||
field.String("check_mode").
|
||||
Default("probe").
|
||||
MaxLen(32).
|
||||
Comment("probe = LLM probe (default); quota = account usage only; quota_probe = both"),
|
||||
// account_id: 配额模式的数据源账号(复用账号侧用量服务,不直接对接上游)。
|
||||
// 普通字段而非 edge(FK 由 SQL 迁移管理);账号删除时数据库置空,
|
||||
// 监控保留并报「账号未关联」。
|
||||
field.Int64("account_id").
|
||||
Optional().
|
||||
Nillable(),
|
||||
field.String("api_mode").
|
||||
Default("chat_completions").
|
||||
MaxLen(32).
|
||||
Comment("OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions"),
|
||||
// endpoint: 探活模式必填(service 层校验);quota 模式存空串
|
||||
// (列保持 NOT NULL,去掉 NotEmpty 校验器即可)。
|
||||
field.String("endpoint").
|
||||
NotEmpty().
|
||||
MaxLen(500).
|
||||
Comment("Provider base origin, e.g. https://api.openai.com"),
|
||||
Comment("Provider base origin, e.g. https://api.openai.com; empty for quota-only monitors"),
|
||||
field.String("api_key_encrypted").
|
||||
NotEmpty().
|
||||
Sensitive().
|
||||
@@ -115,5 +132,6 @@ func (ChannelMonitor) Indexes() []ent.Index {
|
||||
index.Fields("provider", "api_mode"),
|
||||
index.Fields("group_name"),
|
||||
index.Fields("template_id"),
|
||||
index.Fields("account_id"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package schema
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/schema"
|
||||
@@ -43,6 +45,10 @@ func (ChannelMonitorHistory) Fields() []ent.Field {
|
||||
Optional().
|
||||
Default("").
|
||||
MaxLen(500),
|
||||
// quota: 配额模式(check_mode = quota / quota_probe)检测时附带的
|
||||
// 归一化配额快照(domain.MonitorQuotaSnapshot,JSONB);探活模式为 NULL。
|
||||
field.JSON("quota", &domain.MonitorQuotaSnapshot{}).
|
||||
Optional(),
|
||||
field.Time("checked_at").
|
||||
Default(time.Now),
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
|
||||
NotEmpty().
|
||||
MaxLen(100),
|
||||
field.Enum("provider").
|
||||
Values("openai", "anthropic", "gemini", "grok"),
|
||||
Values("openai", "anthropic", "gemini", "grok",
|
||||
"antigravity", "kimi", "zhipu", "deepseek"),
|
||||
field.String("api_mode").
|
||||
Default("chat_completions").
|
||||
MaxLen(32).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
@@ -148,12 +150,51 @@ func (Group) Fields() []ent.Field {
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||
field.JSON("video_model_prices", map[string]map[string]float64{}).
|
||||
Optional().
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("按模型族和分辨率覆盖视频每秒价格"),
|
||||
field.Float("web_search_price_per_call").
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
|
||||
Comment("Codex alpha/search 网页搜索单次价格(USD/次);nil 表示使用默认价 0.01(官方 $10/1000 次)"),
|
||||
|
||||
// 搜索/工具调用显式定价(per 1k calls),用于 Grok web_search 等。
|
||||
field.Float("search_price_per_1k").
|
||||
Optional().
|
||||
Nillable().
|
||||
Min(0).
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
|
||||
Comment("搜索工具价格 per 1000 calls(web_search 等)"),
|
||||
|
||||
// Grok Voice 显式定价(realtime / TTS / STT),不按文本 RateMultiplier。
|
||||
field.Float("audio_realtime_price_per_min").
|
||||
Optional().
|
||||
Nillable().
|
||||
Min(0).
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
|
||||
Comment("Voice realtime 每分钟价格(USD)"),
|
||||
field.Float("audio_tts_price_per_million_chars").
|
||||
Optional().
|
||||
Nillable().
|
||||
Min(0).
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
|
||||
Comment("TTS 每百万字符价格(USD)"),
|
||||
field.Float("audio_stt_price_per_hour").
|
||||
Optional().
|
||||
Nillable().
|
||||
Min(0).
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
|
||||
Comment("STT 每小时价格(USD)"),
|
||||
field.Bool("long_context_pricing_enabled").
|
||||
Default(true).
|
||||
Comment("是否按上下文长度应用模型阶梯价格;默认开启以保持官方/渠道长上下文价"),
|
||||
field.JSON("model_pricing", json.RawMessage{}).
|
||||
Optional().
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("分组逐模型定价;优先级高于渠道和内置定价"),
|
||||
|
||||
// Claude Code 客户端限制 (added by migration 029)
|
||||
field.Bool("claude_code_only").
|
||||
Default(false).
|
||||
@@ -234,6 +275,20 @@ func (Group) Fields() []ent.Field {
|
||||
Default([]domain.ReasoningEffortMapping{}).
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
|
||||
Comment("OpenAI reasoning effort 自定义精确映射;先映射再应用上限"),
|
||||
|
||||
// 分组利润控制(migration 192/193):openai/anthropic/gemini/grok/antigravity
|
||||
// 的 token 分组可启用,composite 分组不能直接启用。
|
||||
field.Bool("profit_control_enabled").
|
||||
Default(false).
|
||||
Comment("是否启用利润控制:调度时仅允许账号计费倍率满足毛利率要求的账号进入候选池"),
|
||||
field.Float("profit_min_margin").
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}).
|
||||
Default(0).
|
||||
Comment("最低毛利率,小数(0.30=30%);账号准入条件为 U <= D*(1-margin-buffer)"),
|
||||
field.Float("profit_safety_buffer").
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}).
|
||||
Default(0).
|
||||
Comment("安全缓冲,小数;与 margin 相加后从下游倍率中扣除,默认 0"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,17 @@ func (UsageLog) Fields() []ent.Field {
|
||||
MaxLen(100).
|
||||
Optional().
|
||||
Nillable(),
|
||||
// UpstreamResponseModel stores the model name declared by the upstream
|
||||
// response before any protocol conversion or client-facing rewrite.
|
||||
field.String("upstream_response_model").
|
||||
MaxLen(200).
|
||||
Optional().
|
||||
Nillable(),
|
||||
// UpstreamModelMismatch is tri-state: NULL means the upstream response did
|
||||
// not declare a model (or predates this field); false/true means observed.
|
||||
field.Bool("upstream_model_mismatch").
|
||||
Optional().
|
||||
Nillable(),
|
||||
field.Int64("channel_id").Optional().Nillable().Comment("渠道 ID"),
|
||||
field.String("model_mapping_chain").MaxLen(500).Optional().Nillable().Comment("模型映射链"),
|
||||
field.String("billing_tier").MaxLen(50).Optional().Nillable().Comment("计费层级标签"),
|
||||
|
||||
@@ -41,7 +41,8 @@ func (UserPlatformQuota) Fields() []ent.Field {
|
||||
// 注意:平台列表的单一权威源为 service.AllowedQuotaPlatforms;
|
||||
// 此处为 ent 构建期约束,需与 service.AllowedQuotaPlatforms 保持同步。
|
||||
switch s {
|
||||
case "anthropic", "openai", "gemini", "antigravity", "grok":
|
||||
case "anthropic", "openai", "gemini", "antigravity", "grok",
|
||||
"kimi", "zhipu", "deepseek":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("platform %q is not allowed", s)
|
||||
|
||||
@@ -4,6 +4,7 @@ package ent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/json/jsontext"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -25,7 +26,7 @@ type UsageCleanupTask struct {
|
||||
// Status holds the value of the "status" field.
|
||||
Status string `json:"status,omitempty"`
|
||||
// Filters holds the value of the "filters" field.
|
||||
Filters json.RawMessage `json:"filters,omitempty"`
|
||||
Filters jsontext.Value `json:"filters,omitempty"`
|
||||
// CreatedBy holds the value of the "created_by" field.
|
||||
CreatedBy int64 `json:"created_by,omitempty"`
|
||||
// DeletedRows holds the value of the "deleted_rows" field.
|
||||
|
||||
@@ -4,7 +4,7 @@ package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/json/jsontext"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -58,7 +58,7 @@ func (_c *UsageCleanupTaskCreate) SetStatus(v string) *UsageCleanupTaskCreate {
|
||||
}
|
||||
|
||||
// SetFilters sets the "filters" field.
|
||||
func (_c *UsageCleanupTaskCreate) SetFilters(v json.RawMessage) *UsageCleanupTaskCreate {
|
||||
func (_c *UsageCleanupTaskCreate) SetFilters(v jsontext.Value) *UsageCleanupTaskCreate {
|
||||
_c.mutation.SetFilters(v)
|
||||
return _c
|
||||
}
|
||||
@@ -375,7 +375,7 @@ func (u *UsageCleanupTaskUpsert) UpdateStatus() *UsageCleanupTaskUpsert {
|
||||
}
|
||||
|
||||
// SetFilters sets the "filters" field.
|
||||
func (u *UsageCleanupTaskUpsert) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsert {
|
||||
func (u *UsageCleanupTaskUpsert) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsert {
|
||||
u.Set(usagecleanuptask.FieldFilters, v)
|
||||
return u
|
||||
}
|
||||
@@ -592,7 +592,7 @@ func (u *UsageCleanupTaskUpsertOne) UpdateStatus() *UsageCleanupTaskUpsertOne {
|
||||
}
|
||||
|
||||
// SetFilters sets the "filters" field.
|
||||
func (u *UsageCleanupTaskUpsertOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertOne {
|
||||
func (u *UsageCleanupTaskUpsertOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertOne {
|
||||
return u.Update(func(s *UsageCleanupTaskUpsert) {
|
||||
s.SetFilters(v)
|
||||
})
|
||||
@@ -999,7 +999,7 @@ func (u *UsageCleanupTaskUpsertBulk) UpdateStatus() *UsageCleanupTaskUpsertBulk
|
||||
}
|
||||
|
||||
// SetFilters sets the "filters" field.
|
||||
func (u *UsageCleanupTaskUpsertBulk) SetFilters(v json.RawMessage) *UsageCleanupTaskUpsertBulk {
|
||||
func (u *UsageCleanupTaskUpsertBulk) SetFilters(v jsontext.Value) *UsageCleanupTaskUpsertBulk {
|
||||
return u.Update(func(s *UsageCleanupTaskUpsert) {
|
||||
s.SetFilters(v)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/json/jsontext"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -51,13 +51,13 @@ func (_u *UsageCleanupTaskUpdate) SetNillableStatus(v *string) *UsageCleanupTask
|
||||
}
|
||||
|
||||
// SetFilters sets the "filters" field.
|
||||
func (_u *UsageCleanupTaskUpdate) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdate {
|
||||
func (_u *UsageCleanupTaskUpdate) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdate {
|
||||
_u.mutation.SetFilters(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AppendFilters appends value to the "filters" field.
|
||||
func (_u *UsageCleanupTaskUpdate) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdate {
|
||||
func (_u *UsageCleanupTaskUpdate) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdate {
|
||||
_u.mutation.AppendFilters(v)
|
||||
return _u
|
||||
}
|
||||
@@ -374,13 +374,13 @@ func (_u *UsageCleanupTaskUpdateOne) SetNillableStatus(v *string) *UsageCleanupT
|
||||
}
|
||||
|
||||
// SetFilters sets the "filters" field.
|
||||
func (_u *UsageCleanupTaskUpdateOne) SetFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne {
|
||||
func (_u *UsageCleanupTaskUpdateOne) SetFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne {
|
||||
_u.mutation.SetFilters(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AppendFilters appends value to the "filters" field.
|
||||
func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v json.RawMessage) *UsageCleanupTaskUpdateOne {
|
||||
func (_u *UsageCleanupTaskUpdateOne) AppendFilters(v jsontext.Value) *UsageCleanupTaskUpdateOne {
|
||||
_u.mutation.AppendFilters(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ type UsageLog struct {
|
||||
RequestedModel *string `json:"requested_model,omitempty"`
|
||||
// UpstreamModel holds the value of the "upstream_model" field.
|
||||
UpstreamModel *string `json:"upstream_model,omitempty"`
|
||||
// UpstreamResponseModel holds the value of the "upstream_response_model" field.
|
||||
UpstreamResponseModel *string `json:"upstream_response_model,omitempty"`
|
||||
// UpstreamModelMismatch holds the value of the "upstream_model_mismatch" field.
|
||||
UpstreamModelMismatch *bool `json:"upstream_model_mismatch,omitempty"`
|
||||
// 渠道 ID
|
||||
ChannelID *int64 `json:"channel_id,omitempty"`
|
||||
// 模型映射链
|
||||
@@ -198,13 +202,13 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
|
||||
switch columns[i] {
|
||||
case usagelog.FieldImageSizeBreakdown:
|
||||
values[i] = new([]byte)
|
||||
case usagelog.FieldLongContextBillingApplied, usagelog.FieldStream, usagelog.FieldCacheTTLOverridden:
|
||||
case usagelog.FieldUpstreamModelMismatch, usagelog.FieldLongContextBillingApplied, usagelog.FieldStream, usagelog.FieldCacheTTLOverridden:
|
||||
values[i] = new(sql.NullBool)
|
||||
case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier, usagelog.FieldAccountRateMultiplier:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldChannelID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount, usagelog.FieldVideoCount, usagelog.FieldVideoDurationSeconds:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldRequestedModel, usagelog.FieldUpstreamModel, usagelog.FieldModelMappingChain, usagelog.FieldBillingTier, usagelog.FieldBillingMode, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldImageSize, usagelog.FieldImageInputSize, usagelog.FieldImageOutputSize, usagelog.FieldImageSizeSource, usagelog.FieldVideoResolution:
|
||||
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldRequestedModel, usagelog.FieldUpstreamModel, usagelog.FieldUpstreamResponseModel, usagelog.FieldModelMappingChain, usagelog.FieldBillingTier, usagelog.FieldBillingMode, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldImageSize, usagelog.FieldImageInputSize, usagelog.FieldImageOutputSize, usagelog.FieldImageSizeSource, usagelog.FieldVideoResolution:
|
||||
values[i] = new(sql.NullString)
|
||||
case usagelog.FieldCreatedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
@@ -273,6 +277,20 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
|
||||
_m.UpstreamModel = new(string)
|
||||
*_m.UpstreamModel = value.String
|
||||
}
|
||||
case usagelog.FieldUpstreamResponseModel:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field upstream_response_model", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UpstreamResponseModel = new(string)
|
||||
*_m.UpstreamResponseModel = value.String
|
||||
}
|
||||
case usagelog.FieldUpstreamModelMismatch:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field upstream_model_mismatch", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UpstreamModelMismatch = new(bool)
|
||||
*_m.UpstreamModelMismatch = value.Bool
|
||||
}
|
||||
case usagelog.FieldChannelID:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field channel_id", values[i])
|
||||
@@ -606,6 +624,16 @@ func (_m *UsageLog) String() string {
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.UpstreamResponseModel; v != nil {
|
||||
builder.WriteString("upstream_response_model=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.UpstreamModelMismatch; v != nil {
|
||||
builder.WriteString("upstream_model_mismatch=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.ChannelID; v != nil {
|
||||
builder.WriteString("channel_id=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
|
||||
@@ -28,6 +28,10 @@ const (
|
||||
FieldRequestedModel = "requested_model"
|
||||
// FieldUpstreamModel holds the string denoting the upstream_model field in the database.
|
||||
FieldUpstreamModel = "upstream_model"
|
||||
// FieldUpstreamResponseModel holds the string denoting the upstream_response_model field in the database.
|
||||
FieldUpstreamResponseModel = "upstream_response_model"
|
||||
// FieldUpstreamModelMismatch holds the string denoting the upstream_model_mismatch field in the database.
|
||||
FieldUpstreamModelMismatch = "upstream_model_mismatch"
|
||||
// FieldChannelID holds the string denoting the channel_id field in the database.
|
||||
FieldChannelID = "channel_id"
|
||||
// FieldModelMappingChain holds the string denoting the model_mapping_chain field in the database.
|
||||
@@ -163,6 +167,8 @@ var Columns = []string{
|
||||
FieldModel,
|
||||
FieldRequestedModel,
|
||||
FieldUpstreamModel,
|
||||
FieldUpstreamResponseModel,
|
||||
FieldUpstreamModelMismatch,
|
||||
FieldChannelID,
|
||||
FieldModelMappingChain,
|
||||
FieldBillingTier,
|
||||
@@ -222,6 +228,8 @@ var (
|
||||
RequestedModelValidator func(string) error
|
||||
// UpstreamModelValidator is a validator for the "upstream_model" field. It is called by the builders before save.
|
||||
UpstreamModelValidator func(string) error
|
||||
// UpstreamResponseModelValidator is a validator for the "upstream_response_model" field. It is called by the builders before save.
|
||||
UpstreamResponseModelValidator func(string) error
|
||||
// ModelMappingChainValidator is a validator for the "model_mapping_chain" field. It is called by the builders before save.
|
||||
ModelMappingChainValidator func(string) error
|
||||
// BillingTierValidator is a validator for the "billing_tier" field. It is called by the builders before save.
|
||||
@@ -327,6 +335,16 @@ func ByUpstreamModel(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUpstreamModel, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUpstreamResponseModel orders the results by the upstream_response_model field.
|
||||
func ByUpstreamResponseModel(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUpstreamResponseModel, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUpstreamModelMismatch orders the results by the upstream_model_mismatch field.
|
||||
func ByUpstreamModelMismatch(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUpstreamModelMismatch, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByChannelID orders the results by the channel_id field.
|
||||
func ByChannelID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldChannelID, opts...).ToFunc()
|
||||
|
||||
@@ -90,6 +90,16 @@ func UpstreamModel(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModel applies equality check predicate on the "upstream_response_model" field. It's identical to UpstreamResponseModelEQ.
|
||||
func UpstreamResponseModel(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelMismatch applies equality check predicate on the "upstream_model_mismatch" field. It's identical to UpstreamModelMismatchEQ.
|
||||
func UpstreamModelMismatch(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldUpstreamModelMismatch, v))
|
||||
}
|
||||
|
||||
// ChannelID applies equality check predicate on the "channel_id" field. It's identical to ChannelIDEQ.
|
||||
func ChannelID(v int64) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldChannelID, v))
|
||||
@@ -615,6 +625,101 @@ func UpstreamModelContainsFold(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldContainsFold(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelEQ applies the EQ predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelEQ(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelNEQ applies the NEQ predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelNEQ(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelIn applies the In predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelIn(vs ...string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIn(FieldUpstreamResponseModel, vs...))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelNotIn applies the NotIn predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelNotIn(vs ...string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotIn(FieldUpstreamResponseModel, vs...))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelGT applies the GT predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelGT(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGT(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelGTE applies the GTE predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelGTE(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGTE(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelLT applies the LT predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelLT(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLT(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelLTE applies the LTE predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelLTE(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLTE(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelContains applies the Contains predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelContains(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldContains(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelHasPrefix applies the HasPrefix predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelHasPrefix(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldHasPrefix(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelHasSuffix applies the HasSuffix predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelHasSuffix(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldHasSuffix(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelIsNil applies the IsNil predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelIsNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIsNull(FieldUpstreamResponseModel))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelNotNil applies the NotNil predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelNotNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotNull(FieldUpstreamResponseModel))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelEqualFold applies the EqualFold predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelEqualFold(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEqualFold(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamResponseModelContainsFold applies the ContainsFold predicate on the "upstream_response_model" field.
|
||||
func UpstreamResponseModelContainsFold(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldContainsFold(FieldUpstreamResponseModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelMismatchEQ applies the EQ predicate on the "upstream_model_mismatch" field.
|
||||
func UpstreamModelMismatchEQ(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldUpstreamModelMismatch, v))
|
||||
}
|
||||
|
||||
// UpstreamModelMismatchNEQ applies the NEQ predicate on the "upstream_model_mismatch" field.
|
||||
func UpstreamModelMismatchNEQ(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldUpstreamModelMismatch, v))
|
||||
}
|
||||
|
||||
// UpstreamModelMismatchIsNil applies the IsNil predicate on the "upstream_model_mismatch" field.
|
||||
func UpstreamModelMismatchIsNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIsNull(FieldUpstreamModelMismatch))
|
||||
}
|
||||
|
||||
// UpstreamModelMismatchNotNil applies the NotNil predicate on the "upstream_model_mismatch" field.
|
||||
func UpstreamModelMismatchNotNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotNull(FieldUpstreamModelMismatch))
|
||||
}
|
||||
|
||||
// ChannelIDEQ applies the EQ predicate on the "channel_id" field.
|
||||
func ChannelIDEQ(v int64) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldChannelID, v))
|
||||
|
||||
@@ -85,6 +85,34 @@ func (_c *UsageLogCreate) SetNillableUpstreamModel(v *string) *UsageLogCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetUpstreamResponseModel sets the "upstream_response_model" field.
|
||||
func (_c *UsageLogCreate) SetUpstreamResponseModel(v string) *UsageLogCreate {
|
||||
_c.mutation.SetUpstreamResponseModel(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableUpstreamResponseModel sets the "upstream_response_model" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableUpstreamResponseModel(v *string) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetUpstreamResponseModel(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetUpstreamModelMismatch sets the "upstream_model_mismatch" field.
|
||||
func (_c *UsageLogCreate) SetUpstreamModelMismatch(v bool) *UsageLogCreate {
|
||||
_c.mutation.SetUpstreamModelMismatch(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableUpstreamModelMismatch sets the "upstream_model_mismatch" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableUpstreamModelMismatch(v *bool) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetUpstreamModelMismatch(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetChannelID sets the "channel_id" field.
|
||||
func (_c *UsageLogCreate) SetChannelID(v int64) *UsageLogCreate {
|
||||
_c.mutation.SetChannelID(v)
|
||||
@@ -788,6 +816,11 @@ func (_c *UsageLogCreate) check() error {
|
||||
return &ValidationError{Name: "upstream_model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.upstream_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _c.mutation.UpstreamResponseModel(); ok {
|
||||
if err := usagelog.UpstreamResponseModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "upstream_response_model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.upstream_response_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _c.mutation.ModelMappingChain(); ok {
|
||||
if err := usagelog.ModelMappingChainValidator(v); err != nil {
|
||||
return &ValidationError{Name: "model_mapping_chain", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model_mapping_chain": %w`, err)}
|
||||
@@ -950,6 +983,14 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(usagelog.FieldUpstreamModel, field.TypeString, value)
|
||||
_node.UpstreamModel = &value
|
||||
}
|
||||
if value, ok := _c.mutation.UpstreamResponseModel(); ok {
|
||||
_spec.SetField(usagelog.FieldUpstreamResponseModel, field.TypeString, value)
|
||||
_node.UpstreamResponseModel = &value
|
||||
}
|
||||
if value, ok := _c.mutation.UpstreamModelMismatch(); ok {
|
||||
_spec.SetField(usagelog.FieldUpstreamModelMismatch, field.TypeBool, value)
|
||||
_node.UpstreamModelMismatch = &value
|
||||
}
|
||||
if value, ok := _c.mutation.ChannelID(); ok {
|
||||
_spec.SetField(usagelog.FieldChannelID, field.TypeInt64, value)
|
||||
_node.ChannelID = &value
|
||||
@@ -1327,6 +1368,42 @@ func (u *UsageLogUpsert) ClearUpstreamModel() *UsageLogUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetUpstreamResponseModel sets the "upstream_response_model" field.
|
||||
func (u *UsageLogUpsert) SetUpstreamResponseModel(v string) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldUpstreamResponseModel, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateUpstreamResponseModel sets the "upstream_response_model" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateUpstreamResponseModel() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldUpstreamResponseModel)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearUpstreamResponseModel clears the value of the "upstream_response_model" field.
|
||||
func (u *UsageLogUpsert) ClearUpstreamResponseModel() *UsageLogUpsert {
|
||||
u.SetNull(usagelog.FieldUpstreamResponseModel)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetUpstreamModelMismatch sets the "upstream_model_mismatch" field.
|
||||
func (u *UsageLogUpsert) SetUpstreamModelMismatch(v bool) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldUpstreamModelMismatch, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateUpstreamModelMismatch sets the "upstream_model_mismatch" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateUpstreamModelMismatch() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldUpstreamModelMismatch)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearUpstreamModelMismatch clears the value of the "upstream_model_mismatch" field.
|
||||
func (u *UsageLogUpsert) ClearUpstreamModelMismatch() *UsageLogUpsert {
|
||||
u.SetNull(usagelog.FieldUpstreamModelMismatch)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetChannelID sets the "channel_id" field.
|
||||
func (u *UsageLogUpsert) SetChannelID(v int64) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldChannelID, v)
|
||||
@@ -2162,6 +2239,48 @@ func (u *UsageLogUpsertOne) ClearUpstreamModel() *UsageLogUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetUpstreamResponseModel sets the "upstream_response_model" field.
|
||||
func (u *UsageLogUpsertOne) SetUpstreamResponseModel(v string) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetUpstreamResponseModel(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUpstreamResponseModel sets the "upstream_response_model" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateUpstreamResponseModel() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateUpstreamResponseModel()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUpstreamResponseModel clears the value of the "upstream_response_model" field.
|
||||
func (u *UsageLogUpsertOne) ClearUpstreamResponseModel() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearUpstreamResponseModel()
|
||||
})
|
||||
}
|
||||
|
||||
// SetUpstreamModelMismatch sets the "upstream_model_mismatch" field.
|
||||
func (u *UsageLogUpsertOne) SetUpstreamModelMismatch(v bool) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetUpstreamModelMismatch(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUpstreamModelMismatch sets the "upstream_model_mismatch" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateUpstreamModelMismatch() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateUpstreamModelMismatch()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUpstreamModelMismatch clears the value of the "upstream_model_mismatch" field.
|
||||
func (u *UsageLogUpsertOne) ClearUpstreamModelMismatch() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearUpstreamModelMismatch()
|
||||
})
|
||||
}
|
||||
|
||||
// SetChannelID sets the "channel_id" field.
|
||||
func (u *UsageLogUpsertOne) SetChannelID(v int64) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
@@ -3276,6 +3395,48 @@ func (u *UsageLogUpsertBulk) ClearUpstreamModel() *UsageLogUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetUpstreamResponseModel sets the "upstream_response_model" field.
|
||||
func (u *UsageLogUpsertBulk) SetUpstreamResponseModel(v string) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetUpstreamResponseModel(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUpstreamResponseModel sets the "upstream_response_model" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateUpstreamResponseModel() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateUpstreamResponseModel()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUpstreamResponseModel clears the value of the "upstream_response_model" field.
|
||||
func (u *UsageLogUpsertBulk) ClearUpstreamResponseModel() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearUpstreamResponseModel()
|
||||
})
|
||||
}
|
||||
|
||||
// SetUpstreamModelMismatch sets the "upstream_model_mismatch" field.
|
||||
func (u *UsageLogUpsertBulk) SetUpstreamModelMismatch(v bool) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetUpstreamModelMismatch(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUpstreamModelMismatch sets the "upstream_model_mismatch" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateUpstreamModelMismatch() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateUpstreamModelMismatch()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearUpstreamModelMismatch clears the value of the "upstream_model_mismatch" field.
|
||||
func (u *UsageLogUpsertBulk) ClearUpstreamModelMismatch() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearUpstreamModelMismatch()
|
||||
})
|
||||
}
|
||||
|
||||
// SetChannelID sets the "channel_id" field.
|
||||
func (u *UsageLogUpsertBulk) SetChannelID(v int64) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
|
||||
@@ -142,6 +142,46 @@ func (_u *UsageLogUpdate) ClearUpstreamModel() *UsageLogUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUpstreamResponseModel sets the "upstream_response_model" field.
|
||||
func (_u *UsageLogUpdate) SetUpstreamResponseModel(v string) *UsageLogUpdate {
|
||||
_u.mutation.SetUpstreamResponseModel(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUpstreamResponseModel sets the "upstream_response_model" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableUpstreamResponseModel(v *string) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetUpstreamResponseModel(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUpstreamResponseModel clears the value of the "upstream_response_model" field.
|
||||
func (_u *UsageLogUpdate) ClearUpstreamResponseModel() *UsageLogUpdate {
|
||||
_u.mutation.ClearUpstreamResponseModel()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUpstreamModelMismatch sets the "upstream_model_mismatch" field.
|
||||
func (_u *UsageLogUpdate) SetUpstreamModelMismatch(v bool) *UsageLogUpdate {
|
||||
_u.mutation.SetUpstreamModelMismatch(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUpstreamModelMismatch sets the "upstream_model_mismatch" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableUpstreamModelMismatch(v *bool) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetUpstreamModelMismatch(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUpstreamModelMismatch clears the value of the "upstream_model_mismatch" field.
|
||||
func (_u *UsageLogUpdate) ClearUpstreamModelMismatch() *UsageLogUpdate {
|
||||
_u.mutation.ClearUpstreamModelMismatch()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetChannelID sets the "channel_id" field.
|
||||
func (_u *UsageLogUpdate) SetChannelID(v int64) *UsageLogUpdate {
|
||||
_u.mutation.ResetChannelID()
|
||||
@@ -1016,6 +1056,11 @@ func (_u *UsageLogUpdate) check() error {
|
||||
return &ValidationError{Name: "upstream_model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.upstream_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.UpstreamResponseModel(); ok {
|
||||
if err := usagelog.UpstreamResponseModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "upstream_response_model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.upstream_response_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.ModelMappingChain(); ok {
|
||||
if err := usagelog.ModelMappingChainValidator(v); err != nil {
|
||||
return &ValidationError{Name: "model_mapping_chain", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model_mapping_chain": %w`, err)}
|
||||
@@ -1108,6 +1153,18 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.UpstreamModelCleared() {
|
||||
_spec.ClearField(usagelog.FieldUpstreamModel, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpstreamResponseModel(); ok {
|
||||
_spec.SetField(usagelog.FieldUpstreamResponseModel, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.UpstreamResponseModelCleared() {
|
||||
_spec.ClearField(usagelog.FieldUpstreamResponseModel, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpstreamModelMismatch(); ok {
|
||||
_spec.SetField(usagelog.FieldUpstreamModelMismatch, field.TypeBool, value)
|
||||
}
|
||||
if _u.mutation.UpstreamModelMismatchCleared() {
|
||||
_spec.ClearField(usagelog.FieldUpstreamModelMismatch, field.TypeBool)
|
||||
}
|
||||
if value, ok := _u.mutation.ChannelID(); ok {
|
||||
_spec.SetField(usagelog.FieldChannelID, field.TypeInt64, value)
|
||||
}
|
||||
@@ -1599,6 +1656,46 @@ func (_u *UsageLogUpdateOne) ClearUpstreamModel() *UsageLogUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUpstreamResponseModel sets the "upstream_response_model" field.
|
||||
func (_u *UsageLogUpdateOne) SetUpstreamResponseModel(v string) *UsageLogUpdateOne {
|
||||
_u.mutation.SetUpstreamResponseModel(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUpstreamResponseModel sets the "upstream_response_model" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableUpstreamResponseModel(v *string) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetUpstreamResponseModel(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUpstreamResponseModel clears the value of the "upstream_response_model" field.
|
||||
func (_u *UsageLogUpdateOne) ClearUpstreamResponseModel() *UsageLogUpdateOne {
|
||||
_u.mutation.ClearUpstreamResponseModel()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUpstreamModelMismatch sets the "upstream_model_mismatch" field.
|
||||
func (_u *UsageLogUpdateOne) SetUpstreamModelMismatch(v bool) *UsageLogUpdateOne {
|
||||
_u.mutation.SetUpstreamModelMismatch(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUpstreamModelMismatch sets the "upstream_model_mismatch" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableUpstreamModelMismatch(v *bool) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetUpstreamModelMismatch(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearUpstreamModelMismatch clears the value of the "upstream_model_mismatch" field.
|
||||
func (_u *UsageLogUpdateOne) ClearUpstreamModelMismatch() *UsageLogUpdateOne {
|
||||
_u.mutation.ClearUpstreamModelMismatch()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetChannelID sets the "channel_id" field.
|
||||
func (_u *UsageLogUpdateOne) SetChannelID(v int64) *UsageLogUpdateOne {
|
||||
_u.mutation.ResetChannelID()
|
||||
@@ -2486,6 +2583,11 @@ func (_u *UsageLogUpdateOne) check() error {
|
||||
return &ValidationError{Name: "upstream_model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.upstream_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.UpstreamResponseModel(); ok {
|
||||
if err := usagelog.UpstreamResponseModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "upstream_response_model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.upstream_response_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.ModelMappingChain(); ok {
|
||||
if err := usagelog.ModelMappingChainValidator(v); err != nil {
|
||||
return &ValidationError{Name: "model_mapping_chain", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model_mapping_chain": %w`, err)}
|
||||
@@ -2595,6 +2697,18 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
|
||||
if _u.mutation.UpstreamModelCleared() {
|
||||
_spec.ClearField(usagelog.FieldUpstreamModel, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpstreamResponseModel(); ok {
|
||||
_spec.SetField(usagelog.FieldUpstreamResponseModel, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.UpstreamResponseModelCleared() {
|
||||
_spec.ClearField(usagelog.FieldUpstreamResponseModel, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.UpstreamModelMismatch(); ok {
|
||||
_spec.SetField(usagelog.FieldUpstreamModelMismatch, field.TypeBool, value)
|
||||
}
|
||||
if _u.mutation.UpstreamModelMismatchCleared() {
|
||||
_spec.ClearField(usagelog.FieldUpstreamModelMismatch, field.TypeBool)
|
||||
}
|
||||
if value, ok := _u.mutation.ChannelID(); ok {
|
||||
_spec.SetField(usagelog.FieldChannelID, field.TypeInt64, value)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
module github.com/Wei-Shaw/sub2api
|
||||
|
||||
go 1.26.5
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
|
||||
github.com/alibabacloud-go/tea v1.3.13
|
||||
github.com/alicebob/miniredis/v2 v2.38.0
|
||||
github.com/alitto/pond/v2 v2.6.2
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
@@ -21,6 +24,8 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-hclog v1.6.3
|
||||
github.com/hashicorp/go-plugin v1.8.0
|
||||
github.com/imroc/req/v3 v3.59.0
|
||||
github.com/klauspost/compress v1.18.2
|
||||
github.com/lib/pq v1.10.9
|
||||
@@ -35,6 +40,8 @@ require (
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stripe/stripe-go/v85 v85.0.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.3.52
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.52
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
|
||||
github.com/testcontainers/testcontainers-go/modules/redis v0.40.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
@@ -49,6 +56,8 @@ require (
|
||||
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
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.44.3
|
||||
@@ -60,6 +69,10 @@ require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.5 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
@@ -81,6 +94,7 @@ require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
@@ -111,12 +125,14 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.6 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
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
|
||||
@@ -139,6 +155,7 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
@@ -165,6 +182,7 @@ require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
@@ -176,10 +194,9 @@ require (
|
||||
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.41.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.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.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
@@ -189,8 +206,7 @@ require (
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc=
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
@@ -8,6 +9,7 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -16,10 +18,56 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3 h1:0Aobw12m3x28aeDMPjwjXsfF8MuLvRjlQ4Hhoy5hFOY=
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3/go.mod h1:ydzBIN2OiM7eeQPpAFyBrv1H5TY1MtUP2rQig44C4UQ=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
|
||||
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/alitto/pond/v2 v2.6.2 h1:Sphe40g0ILeM1pA2c2K+Th0DGU+pt0A/Kprr+WB24Pw=
|
||||
github.com/alitto/pond/v2 v2.6.2/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
|
||||
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
@@ -72,16 +120,23 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -120,6 +175,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -163,18 +222,36 @@ github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
||||
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
@@ -182,10 +259,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs=
|
||||
github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
@@ -193,6 +276,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||
github.com/imroc/req/v3 v3.59.0 h1:PqKhJHyBmJYob47LVuTHwRZE00ZO6icbLHe5Zra13jo=
|
||||
@@ -205,8 +290,12 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
|
||||
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
@@ -215,6 +304,8 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
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=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
@@ -227,8 +318,12 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
@@ -259,12 +354,17 @@ github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -286,6 +386,7 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
@@ -322,6 +423,9 @@ github.com/smartwalle/ngx v1.1.0 h1:q8nANgWSPRGeI/u+ixBoA4mf68DrUq6vZ+n9L5UKv9I=
|
||||
github.com/smartwalle/ngx v1.1.0/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
|
||||
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
|
||||
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
@@ -335,13 +439,16 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
@@ -353,6 +460,10 @@ github.com/stripe/stripe-go/v85 v85.0.0 h1:HMlFJXW6I/9WvkeSAtj8V7dI5pzeDu4gS1Taq
|
||||
github.com/stripe/stripe-go/v85 v85.0.0/go.mod h1:5P+HGFenpWgak27T5Is6JMsmDfUC1yJnjhhmquz7kXw=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.3.52 h1:bPz4h9cPAD2psXNdVNHZUM/V13P05rtXoFIFn1IIPoA=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.3.52/go.mod h1:KDlcSxrt2pw9nenvXpw/VHipuokbA0j2iRXV+2gF5j8=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.52 h1:agyo5WB5bclK346U0Y4G40c//eA5qZbtBVGdp3HhuK8=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.52/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk=
|
||||
@@ -372,6 +483,9 @@ github.com/tiktoken-go/tokenizer v0.8.0 h1:drHWno2Zx3eAm/hk/LmvBKXPpSImB7BRyh/ru
|
||||
github.com/tiktoken-go/tokenizer v0.8.0/go.mod h1:pTmPz4r14MV3JkUGAmAcdLdYhSxN68MCjrP+EoxBdx0=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
@@ -386,6 +500,9 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
@@ -400,18 +517,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
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.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
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/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.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
|
||||
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
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/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=
|
||||
@@ -429,59 +548,196 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
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/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=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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/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=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
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/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=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/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=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/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=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
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/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=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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/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=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
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/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=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
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-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/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/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=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
|
||||
@@ -32,7 +32,7 @@ const (
|
||||
|
||||
// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
|
||||
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
|
||||
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src 'self' https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
|
||||
// UMQ(用户消息队列)模式常量
|
||||
const (
|
||||
@@ -61,6 +61,10 @@ const (
|
||||
// 可通过 gateway.upstream_response_read_max_bytes 配置项覆盖。
|
||||
const DefaultUpstreamResponseReadMaxBytes int64 = 128 * 1024 * 1024
|
||||
|
||||
// DefaultModelsListReadMaxBytes 上游模型列表响应体的默认读取上限。
|
||||
// 可通过 gateway.models_list_read_max_bytes 配置项覆盖。
|
||||
const DefaultModelsListReadMaxBytes int64 = 8 * 1024 * 1024
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
@@ -99,6 +103,18 @@ type Config struct {
|
||||
Idempotency IdempotencyConfig `mapstructure:"idempotency"`
|
||||
BatchImage BatchImageConfig `mapstructure:"batch_image"`
|
||||
ImageStorage ImageStorageConfig `mapstructure:"image_storage"`
|
||||
Plugins PluginConfig `mapstructure:"plugins"`
|
||||
}
|
||||
|
||||
// PluginConfig 控制管理员手动上传的本地进程插件。
|
||||
// 默认不包含插件,也不允许安装未签名插件;TrustedPublishers 用于追加第三方发布者。
|
||||
type PluginConfig struct {
|
||||
DataDir string `mapstructure:"data_dir"`
|
||||
AllowUnsigned bool `mapstructure:"allow_unsigned"`
|
||||
TrustedPublishers map[string]string `mapstructure:"trusted_publishers"`
|
||||
MaxUploadBytes int64 `mapstructure:"max_upload_bytes"`
|
||||
MaxUncompressedBytes int64 `mapstructure:"max_uncompressed_bytes"`
|
||||
StartTimeoutSeconds int `mapstructure:"start_timeout_seconds"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
@@ -830,6 +846,53 @@ type ProxyFallbackConfig struct {
|
||||
|
||||
type ProxyProbeConfig struct {
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
|
||||
// URLs 按优先级排列的自定义探测 URL 列表。
|
||||
// 留空时使用内置默认列表(ip-api → ipify)。
|
||||
// 某些 AI API 专用代理只允许访问特定域名,配置多个备选可提高探测成功率。
|
||||
URLs []ProbeURLConfig `mapstructure:"urls"`
|
||||
}
|
||||
|
||||
// ProbeURLConfig 描述一个探测端点及其响应解析方式。
|
||||
type ProbeURLConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
Parser string `mapstructure:"parser"` // "ip-api" / "ipify" / "chatgpt-trace"
|
||||
}
|
||||
|
||||
func normalizeProxyProbeURLs(targets []ProbeURLConfig) ([]ProbeURLConfig, error) {
|
||||
if len(targets) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
normalized := make([]ProbeURLConfig, 0, len(targets))
|
||||
for i, target := range targets {
|
||||
rawURL := strings.TrimSpace(target.URL)
|
||||
parser := strings.ToLower(strings.TrimSpace(target.Parser))
|
||||
if rawURL == "" {
|
||||
return nil, fmt.Errorf("entry %d: url is required", i)
|
||||
}
|
||||
if parser == "" {
|
||||
return nil, fmt.Errorf("entry %d: parser is required", i)
|
||||
}
|
||||
switch parser {
|
||||
case "ip-api", "ipify", "chatgpt-trace":
|
||||
default:
|
||||
return nil, fmt.Errorf("entry %d: unsupported parser %q", i, target.Parser)
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed.Host == "" {
|
||||
return nil, fmt.Errorf("entry %d: invalid url %q", i, target.URL)
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return nil, fmt.Errorf("entry %d: url scheme must be http or https", i)
|
||||
}
|
||||
|
||||
normalized = append(normalized, ProbeURLConfig{
|
||||
URL: rawURL,
|
||||
Parser: parser,
|
||||
})
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
type BillingConfig struct {
|
||||
@@ -887,6 +950,9 @@ type GatewayConfig struct {
|
||||
// OpenAIResponseHeaderTimeout: OpenAI/Codex 上游等待响应头的超时时间(秒),0表示无超时
|
||||
// OpenAI/Codex 请求可能在上游排队较久;默认不使用通用响应头超时截断。
|
||||
OpenAIResponseHeaderTimeout int `mapstructure:"openai_response_header_timeout"`
|
||||
// GrokResponseHeaderTimeout bounds the pre-first-byte wait for xAI/Grok.
|
||||
// A zero value uses the provider-safe default instead of the generic gateway timeout.
|
||||
GrokResponseHeaderTimeout int `mapstructure:"grok_response_header_timeout"`
|
||||
// OpenAIFirstOutputTimeoutSeconds: native HTTP Responses 首个语义输出超时(秒),0表示禁用。
|
||||
OpenAIFirstOutputTimeoutSeconds int `mapstructure:"openai_first_output_timeout_seconds"`
|
||||
// OpenAIHighEffortFirstOutputTimeoutSeconds: high/xhigh/max 推理的首个语义输出超时(秒)。
|
||||
@@ -898,6 +964,8 @@ type GatewayConfig struct {
|
||||
TextMaxBodySize int64 `mapstructure:"text_max_body_size"`
|
||||
// 非流式上游响应体读取上限(字节),用于防止无界读取导致内存放大
|
||||
UpstreamResponseReadMaxBytes int64 `mapstructure:"upstream_response_read_max_bytes"`
|
||||
// 上游模型列表响应体读取上限(字节)
|
||||
ModelsListReadMaxBytes int64 `mapstructure:"models_list_read_max_bytes"`
|
||||
// 代理探测响应体读取上限(字节)
|
||||
ProxyProbeResponseReadMaxBytes int64 `mapstructure:"proxy_probe_response_read_max_bytes"`
|
||||
// Gemini 上游响应头调试日志开关(默认关闭,避免高频日志开销)
|
||||
@@ -907,6 +975,18 @@ type GatewayConfig struct {
|
||||
// ForceCodexCLI: 强制将 OpenAI `/v1/responses` 请求按 Codex CLI 处理。
|
||||
// 用于网关未透传/改写 User-Agent 时的兼容兜底(默认关闭,避免影响其他客户端)。
|
||||
ForceCodexCLI bool `mapstructure:"force_codex_cli"`
|
||||
// DisableCodexIdentityEnforcement: 关闭「强制统一 Codex 出站身份」。上游 /backend-api/codex
|
||||
// 在容量紧张时按客户端身份分优先级降载,被降载的请求会拿到 HTTP 200 + 流内
|
||||
// server_is_overloaded,该次请求失败。默认强制统一出口:所有 OAuth 出站的
|
||||
// User-Agent / originator / version 都改写为网关规范身份,确保没有请求带着第三方或陈旧身份
|
||||
// 出站。置 true 后退回「仅按最终 User-Agent 配对 originator」的收口语义,供上游策略变动时回滚。
|
||||
//
|
||||
// 取反义命名是为了让零值安全:该开关会发布为进程级快照,未经 viper 加载而手工构造的
|
||||
// Config(测试、工具)其零值必须落在「强制统一开启」这一侧,否则会静默丢掉这层保护。
|
||||
DisableCodexIdentityEnforcement bool `mapstructure:"disable_codex_identity_enforcement"`
|
||||
// DisableCodexOriginatorNormalization: 已废弃,等价于 DisableCodexIdentityEnforcement。
|
||||
// 保留以兼容既有配置文件;加载时会折叠进新键,不要在新代码里直接读取。
|
||||
DisableCodexOriginatorNormalization bool `mapstructure:"disable_codex_originator_normalization"`
|
||||
// CodexImageGenerationBridgeEnabled: 是否为 Codex `/v1/responses` 自动注入 image_generation 工具和桥接指令。
|
||||
// 默认关闭,避免纯文本 Codex 请求被意外改写;显式携带 image_generation 工具的请求仍按分组能力转发。
|
||||
CodexImageGenerationBridgeEnabled bool `mapstructure:"codex_image_generation_bridge_enabled"`
|
||||
@@ -1010,6 +1090,55 @@ type GatewayConfig struct {
|
||||
// UserMessageQueue: 用户消息串行队列配置
|
||||
// 对 role:"user" 的真实用户消息实施账号级串行化 + RPM 自适应延迟
|
||||
UserMessageQueue UserMessageQueueConfig `mapstructure:"user_message_queue"`
|
||||
|
||||
// Grok: Grok/xAI gateway scheduling and free-tier soft-gate settings.
|
||||
Grok GatewayGrokConfig `mapstructure:"grok"`
|
||||
|
||||
// CNProviders: 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
|
||||
// 仅作用于 payg(按量付费)账号:周期探测余额,低于阈值则临时停调。
|
||||
CNProviders GatewayCNProvidersConfig `mapstructure:"cn_providers"`
|
||||
}
|
||||
|
||||
// GatewayGrokConfig holds Grok-specific gateway scheduling knobs.
|
||||
//
|
||||
// Free-quota soft gate keys (gateway.grok.*):
|
||||
// - free_quota_soft_gate_enabled: enable local rolling-window scheduling guard for
|
||||
// OAuth accounts whose subscription_tier/plan_type is explicitly "free".
|
||||
// Default true is safe only because free-tier detection is strict (unknown/paid fail open).
|
||||
// - free_quota_token_limit: nominal rolling-window token allowance.
|
||||
// - free_quota_soft_gate_percent: stop new scheduling before the nominal limit (1-100).
|
||||
// - free_quota_window_hours: local usage rolling window length in hours.
|
||||
// - free_quota_stats_cache_seconds: cache TTL for free-tier usage stats
|
||||
// (hot path never blocks on DB; misses fail open and refresh in background).
|
||||
type GatewayGrokConfig struct {
|
||||
// PasswordAuthEnabled controls the optional password-to-SSO OAuth flow.
|
||||
// It defaults to false and must be explicitly enabled by the operator.
|
||||
// When true, POST /admin/grok/oauth/password is functional (not ignored).
|
||||
PasswordAuthEnabled bool `mapstructure:"password_auth_enabled"`
|
||||
// FreeQuotaSoftGateEnabled enables a local rolling-window scheduling guard
|
||||
// for explicitly free Grok OAuth accounts only.
|
||||
FreeQuotaSoftGateEnabled bool `mapstructure:"free_quota_soft_gate_enabled"`
|
||||
// FreeQuotaTokenLimit is the nominal rolling-window allowance.
|
||||
FreeQuotaTokenLimit int64 `mapstructure:"free_quota_token_limit"`
|
||||
// FreeQuotaSoftGatePercent stops new scheduling before the nominal limit.
|
||||
FreeQuotaSoftGatePercent int `mapstructure:"free_quota_soft_gate_percent"`
|
||||
// FreeQuotaWindowHours controls the local rolling usage window.
|
||||
FreeQuotaWindowHours int `mapstructure:"free_quota_window_hours"`
|
||||
// FreeQuotaStatsCacheSeconds is the soft-gate stats cache TTL. Hot path never
|
||||
// waits on usage_logs; misses fail open and refresh asynchronously.
|
||||
FreeQuotaStatsCacheSeconds int `mapstructure:"free_quota_stats_cache_seconds"`
|
||||
}
|
||||
|
||||
// GatewayCNProvidersConfig 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
|
||||
//
|
||||
// 仅作用于 payg(按量付费)账号(kimi/deepseek 有公开余额端点;zhipu 无,仅靠响应式 429/402)。
|
||||
// - balance_check_enabled: 是否启用周期余额检测(默认 true)
|
||||
// - balance_threshold: 余额低于此值(账户货币单位,默认 0.5)触发临时停调
|
||||
// - balance_check_interval_minutes: 余额检测周期(分钟,默认 10)
|
||||
type GatewayCNProvidersConfig struct {
|
||||
BalanceCheckEnabled bool `mapstructure:"balance_check_enabled"`
|
||||
BalanceThreshold float64 `mapstructure:"balance_threshold"`
|
||||
BalanceCheckIntervalMinutes int `mapstructure:"balance_check_interval_minutes"`
|
||||
}
|
||||
|
||||
type GatewayLiveConfig struct {
|
||||
@@ -1658,6 +1787,10 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||
// 环境变量支持
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
if tz, ok := os.LookupEnv("TZ"); ok && strings.TrimSpace(tz) != "" {
|
||||
// AutomaticEnv 会先把 timezone 映射到 TIMEZONE;显式 Set 保证标准 TZ 变量优先。
|
||||
viper.Set("timezone", strings.TrimSpace(tz))
|
||||
}
|
||||
if err := viper.BindEnv("server.enable_server_timing", "ENABLE_SERVER_TIMING"); err != nil {
|
||||
return nil, fmt.Errorf("bind ENABLE_SERVER_TIMING: %w", err)
|
||||
}
|
||||
@@ -1766,6 +1899,13 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||
cfg.Gateway.ForcedCodexInstructionsTemplate = string(content)
|
||||
}
|
||||
|
||||
// 兼容旧键 gateway.disable_codex_originator_normalization:语义已被
|
||||
// disable_codex_identity_enforcement 取代(身份改写升级为强制统一出口),
|
||||
// 任一为 true 即关闭强制统一。
|
||||
if cfg.Gateway.DisableCodexOriginatorNormalization {
|
||||
cfg.Gateway.DisableCodexIdentityEnforcement = true
|
||||
}
|
||||
|
||||
// 兼容旧键 gateway.openai_ws.sticky_previous_response_ttl_seconds。
|
||||
// 新键未配置(<=0)时回退旧键;新键优先。
|
||||
if cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds <= 0 && cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds > 0 {
|
||||
@@ -2147,6 +2287,14 @@ func setDefaults() {
|
||||
viper.SetDefault("pricing.update_interval_hours", 24)
|
||||
viper.SetDefault("pricing.hash_check_interval_minutes", 10)
|
||||
|
||||
// 本地进程插件。插件必须由管理员手动上传,项目默认不携带任何插件能力。
|
||||
viper.SetDefault("plugins.data_dir", "")
|
||||
viper.SetDefault("plugins.allow_unsigned", false)
|
||||
viper.SetDefault("plugins.trusted_publishers", map[string]string{})
|
||||
viper.SetDefault("plugins.max_upload_bytes", int64(128*1024*1024))
|
||||
viper.SetDefault("plugins.max_uncompressed_bytes", int64(256*1024*1024))
|
||||
viper.SetDefault("plugins.start_timeout_seconds", 15)
|
||||
|
||||
// Timezone (default to Asia/Shanghai for Chinese users)
|
||||
viper.SetDefault("timezone", "Asia/Shanghai")
|
||||
|
||||
@@ -2208,6 +2356,7 @@ func setDefaults() {
|
||||
// Gateway
|
||||
viper.SetDefault("gateway.response_header_timeout", 600) // 600秒(10分钟)等待上游响应头,LLM高负载时可能排队较久
|
||||
viper.SetDefault("gateway.openai_response_header_timeout", 0)
|
||||
viper.SetDefault("gateway.grok_response_header_timeout", 120)
|
||||
viper.SetDefault("gateway.openai_first_output_timeout_seconds", 0)
|
||||
viper.SetDefault("gateway.openai_high_effort_first_output_timeout_seconds", 0)
|
||||
viper.SetDefault("gateway.log_upstream_error_body", true)
|
||||
@@ -2217,6 +2366,8 @@ func setDefaults() {
|
||||
viper.SetDefault("gateway.max_account_switches", 10)
|
||||
viper.SetDefault("gateway.max_account_switches_gemini", 3)
|
||||
viper.SetDefault("gateway.force_codex_cli", false)
|
||||
viper.SetDefault("gateway.disable_codex_identity_enforcement", false)
|
||||
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")
|
||||
@@ -2288,6 +2439,19 @@ func setDefaults() {
|
||||
viper.SetDefault("gateway.openai_proxy_stream_circuit.failure_threshold", 2)
|
||||
viper.SetDefault("gateway.openai_proxy_stream_circuit.window_seconds", 60)
|
||||
viper.SetDefault("gateway.openai_proxy_stream_circuit.ttl_seconds", 600)
|
||||
// Grok free-tier local soft gate (scheduler-only; admin QueryQuota does not use this).
|
||||
// Enabled by default because free detection requires an explicit free tier marker.
|
||||
viper.SetDefault("gateway.grok.free_quota_soft_gate_enabled", true)
|
||||
viper.SetDefault("gateway.grok.password_auth_enabled", false)
|
||||
// Free soft-gate nominal limit: 500k tokens / rolling 24h (operator policy).
|
||||
viper.SetDefault("gateway.grok.free_quota_token_limit", int64(500_000))
|
||||
viper.SetDefault("gateway.grok.free_quota_soft_gate_percent", 95)
|
||||
viper.SetDefault("gateway.grok.free_quota_window_hours", 24)
|
||||
viper.SetDefault("gateway.grok.free_quota_stats_cache_seconds", 60)
|
||||
// 国产供应商余额检测(kimi/deepseek payg;zhipu 无余额端点,仅靠响应式 429/402)。
|
||||
viper.SetDefault("gateway.cn_providers.balance_check_enabled", true)
|
||||
viper.SetDefault("gateway.cn_providers.balance_threshold", 0.5)
|
||||
viper.SetDefault("gateway.cn_providers.balance_check_interval_minutes", 10)
|
||||
viper.SetDefault("gateway.image_concurrency.enabled", false)
|
||||
viper.SetDefault("gateway.image_concurrency.max_concurrent_requests", 0)
|
||||
viper.SetDefault("gateway.image_concurrency.overflow_mode", ImageConcurrencyOverflowModeReject)
|
||||
@@ -2298,6 +2462,7 @@ func setDefaults() {
|
||||
viper.SetDefault("gateway.max_body_size", int64(256*1024*1024))
|
||||
viper.SetDefault("gateway.text_max_body_size", int64(32*1024*1024))
|
||||
viper.SetDefault("gateway.upstream_response_read_max_bytes", DefaultUpstreamResponseReadMaxBytes)
|
||||
viper.SetDefault("gateway.models_list_read_max_bytes", DefaultModelsListReadMaxBytes)
|
||||
viper.SetDefault("gateway.proxy_probe_response_read_max_bytes", int64(1024*1024))
|
||||
viper.SetDefault("gateway.gemini_debug_response_headers", false)
|
||||
viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
|
||||
@@ -2482,6 +2647,20 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
c.Security.ForwardedClientIPHeaders = forwardedClientIPHeaders
|
||||
c.SetForwardedClientIPSettings(c.Security.TrustForwardedIPForAPIKeyACL, forwardedClientIPHeaders)
|
||||
proxyProbeURLs, err := normalizeProxyProbeURLs(c.Security.ProxyProbe.URLs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("security.proxy_probe.urls: %w", err)
|
||||
}
|
||||
c.Security.ProxyProbe.URLs = proxyProbeURLs
|
||||
if c.Plugins.MaxUploadBytes <= 0 || c.Plugins.MaxUploadBytes > 1024*1024*1024 {
|
||||
return fmt.Errorf("plugins.max_upload_bytes must be between 1 and 1073741824")
|
||||
}
|
||||
if c.Plugins.MaxUncompressedBytes < c.Plugins.MaxUploadBytes || c.Plugins.MaxUncompressedBytes > 2*1024*1024*1024 {
|
||||
return fmt.Errorf("plugins.max_uncompressed_bytes must be between max_upload_bytes and 2147483648")
|
||||
}
|
||||
if c.Plugins.StartTimeoutSeconds < 1 || c.Plugins.StartTimeoutSeconds > 120 {
|
||||
return fmt.Errorf("plugins.start_timeout_seconds must be between 1 and 120")
|
||||
}
|
||||
if c.Server.ReadHeaderTimeout < 1 || c.Server.ReadHeaderTimeout > 60 {
|
||||
return fmt.Errorf("server.read_header_timeout must be between 1 and 60 seconds")
|
||||
}
|
||||
@@ -3093,6 +3272,9 @@ func (c *Config) Validate() error {
|
||||
if c.Gateway.UpstreamResponseReadMaxBytes <= 0 {
|
||||
return fmt.Errorf("gateway.upstream_response_read_max_bytes must be positive")
|
||||
}
|
||||
if c.Gateway.ModelsListReadMaxBytes <= 0 {
|
||||
return fmt.Errorf("gateway.models_list_read_max_bytes must be positive")
|
||||
}
|
||||
if c.Gateway.ProxyProbeResponseReadMaxBytes <= 0 {
|
||||
return fmt.Errorf("gateway.proxy_probe_response_read_max_bytes must be positive")
|
||||
}
|
||||
@@ -3102,6 +3284,9 @@ func (c *Config) Validate() error {
|
||||
if c.Gateway.OpenAIResponseHeaderTimeout < 0 {
|
||||
return fmt.Errorf("gateway.openai_response_header_timeout must be non-negative")
|
||||
}
|
||||
if c.Gateway.GrokResponseHeaderTimeout < 0 || c.Gateway.GrokResponseHeaderTimeout > 1800 {
|
||||
return fmt.Errorf("gateway.grok_response_header_timeout must be between 0-1800 seconds")
|
||||
}
|
||||
if c.Gateway.OpenAIFirstOutputTimeoutSeconds < 0 || c.Gateway.OpenAIFirstOutputTimeoutSeconds > 600 ||
|
||||
(c.Gateway.OpenAIFirstOutputTimeoutSeconds > 0 && c.Gateway.OpenAIFirstOutputTimeoutSeconds < 30) {
|
||||
return fmt.Errorf("gateway.openai_first_output_timeout_seconds must be 0 or between 30-600 seconds")
|
||||
@@ -3497,6 +3682,20 @@ func (c *Config) Validate() error {
|
||||
if c.Concurrency.PingInterval < 5 || c.Concurrency.PingInterval > 30 {
|
||||
return fmt.Errorf("concurrency.ping_interval must be between 5-30 seconds")
|
||||
}
|
||||
if c.Gateway.Grok.FreeQuotaSoftGateEnabled {
|
||||
if c.Gateway.Grok.FreeQuotaTokenLimit <= 0 {
|
||||
return fmt.Errorf("gateway.grok.free_quota_token_limit must be positive")
|
||||
}
|
||||
if c.Gateway.Grok.FreeQuotaSoftGatePercent < 1 || c.Gateway.Grok.FreeQuotaSoftGatePercent > 100 {
|
||||
return fmt.Errorf("gateway.grok.free_quota_soft_gate_percent must be between 1 and 100")
|
||||
}
|
||||
if c.Gateway.Grok.FreeQuotaWindowHours <= 0 {
|
||||
return fmt.Errorf("gateway.grok.free_quota_window_hours must be positive")
|
||||
}
|
||||
}
|
||||
if c.Gateway.Grok.FreeQuotaStatsCacheSeconds < 0 {
|
||||
return fmt.Errorf("gateway.grok.free_quota_stats_cache_seconds must be non-negative")
|
||||
}
|
||||
if err := ValidateDingTalkConfig(c.DingTalk); err != nil {
|
||||
return fmt.Errorf("dingtalk_connect: %w", err)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,45 @@ func resetViperWithJWTSecret(t *testing.T) {
|
||||
t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
|
||||
}
|
||||
|
||||
func TestLoadDefaultModelsListReadMaxBytes(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, DefaultModelsListReadMaxBytes, cfg.Gateway.ModelsListReadMaxBytes)
|
||||
}
|
||||
|
||||
func TestLoadTimezonePrecedence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileTimezone string
|
||||
timezoneEnv string
|
||||
tzEnv string
|
||||
want string
|
||||
}{
|
||||
{name: "default", want: "Asia/Shanghai"},
|
||||
{name: "config_file", fileTimezone: "Europe/London", want: "Europe/London"},
|
||||
{name: "timezone_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", want: "UTC"},
|
||||
{name: "tz_env", fileTimezone: "Europe/London", timezoneEnv: "UTC", tzEnv: "America/New_York", want: "America/New_York"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("TIMEZONE", tt.timezoneEnv)
|
||||
t.Setenv("TZ", tt.tzEnv)
|
||||
if tt.fileTimezone != "" {
|
||||
configFile := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(configFile, []byte("timezone: "+tt.fileTimezone+"\n"), 0o600))
|
||||
t.Setenv("CONFIG_FILE", configFile)
|
||||
}
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, cfg.Timezone)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadServerTimingConfig(t *testing.T) {
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
@@ -521,6 +560,15 @@ func TestLoadOpenAIWSClientFirstMessageTimeoutFromEnv(t *testing.T) {
|
||||
require.Equal(t, 120, cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds)
|
||||
}
|
||||
|
||||
func TestLoadOpenAIWSForceHTTPFromEnv(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("GATEWAY_OPENAI_WS_FORCE_HTTP", "true")
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.True(t, cfg.Gateway.OpenAIWS.ForceHTTP)
|
||||
}
|
||||
|
||||
func TestLoadDefaultOpenAICompactModel(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
@@ -538,6 +586,19 @@ func TestLoadOpenAICompactModelFromEnv(t *testing.T) {
|
||||
require.Equal(t, "gpt-5.3-codex", cfg.Gateway.OpenAICompactModel)
|
||||
}
|
||||
|
||||
func TestLoadDefaultGrokFreeQuotaSoftGate(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.False(t, cfg.Gateway.Grok.PasswordAuthEnabled)
|
||||
require.True(t, cfg.Gateway.Grok.FreeQuotaSoftGateEnabled)
|
||||
require.Equal(t, int64(500_000), cfg.Gateway.Grok.FreeQuotaTokenLimit)
|
||||
require.Equal(t, 95, cfg.Gateway.Grok.FreeQuotaSoftGatePercent)
|
||||
require.Equal(t, 24, cfg.Gateway.Grok.FreeQuotaWindowHours)
|
||||
require.Equal(t, 60, cfg.Gateway.Grok.FreeQuotaStatsCacheSeconds)
|
||||
}
|
||||
|
||||
func TestLoadDefaultOpenAIHTTP2Enabled(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
|
||||
@@ -1749,6 +1810,11 @@ func TestValidateConfigErrors(t *testing.T) {
|
||||
mutate: func(c *Config) { c.Gateway.TextMaxBodySize = c.Gateway.MaxBodySize + 1 },
|
||||
wantErr: "gateway.text_max_body_size",
|
||||
},
|
||||
{
|
||||
name: "gateway models list read limit",
|
||||
mutate: func(c *Config) { c.Gateway.ModelsListReadMaxBytes = 0 },
|
||||
wantErr: "gateway.models_list_read_max_bytes",
|
||||
},
|
||||
{
|
||||
name: "gateway response header timeout",
|
||||
mutate: func(c *Config) { c.Gateway.ResponseHeaderTimeout = -1 },
|
||||
|
||||
@@ -45,6 +45,20 @@ func collectMapstructureKeys(t reflect.Type, prefix string, out map[string]strin
|
||||
// is out of scope here — such settings need a config file either way.
|
||||
continue
|
||||
}
|
||||
if ft.Kind() == reflect.Slice {
|
||||
elem := ft.Elem()
|
||||
for elem.Kind() == reflect.Ptr {
|
||||
elem = elem.Elem()
|
||||
}
|
||||
if elem.Kind() == reflect.Struct {
|
||||
// AutomaticEnv exposes one string value. Viper's string-to-slice
|
||||
// hook can populate scalar slices, but it cannot decode a string
|
||||
// into []struct. Registering a default would turn silent ignore
|
||||
// into a startup unmarshal error, so structured slices remain
|
||||
// config-file-only just like maps.
|
||||
continue
|
||||
}
|
||||
}
|
||||
out[strings.ToLower(key)] = ft.String()
|
||||
}
|
||||
}
|
||||
@@ -62,7 +76,7 @@ func collectMapstructureKeys(t reflect.Type, prefix string, out map[string]strin
|
||||
// were lost, silently disabling async image tasks for env-driven deployments.
|
||||
//
|
||||
// When this fails, register a zero-valued default in setEnvReachableDefaults
|
||||
// for each reported key.
|
||||
// for each reported scalar key. Maps and slices of structs are config-file-only.
|
||||
func TestConfigKeysAreEnvReachable(t *testing.T) {
|
||||
bound := map[string]string{}
|
||||
collectMapstructureKeys(reflect.TypeOf(Config{}), "", bound)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//go:build unit
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeProxyProbeURLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := normalizeProxyProbeURLs([]ProbeURLConfig{
|
||||
{URL: " https://chatgpt.com/cdn-cgi/trace ", Parser: " CHATGPT-TRACE "},
|
||||
{URL: "https://api64.ipify.org?format=json", Parser: "ipify"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []ProbeURLConfig{
|
||||
{URL: "https://chatgpt.com/cdn-cgi/trace", Parser: "chatgpt-trace"},
|
||||
{URL: "https://api64.ipify.org?format=json", Parser: "ipify"},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestNormalizeProxyProbeURLsRejectsInvalidEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
target ProbeURLConfig
|
||||
wantErr string
|
||||
}{
|
||||
{name: "missing URL", target: ProbeURLConfig{Parser: "ipify"}, wantErr: "url is required"},
|
||||
{name: "missing parser", target: ProbeURLConfig{URL: "https://example.com"}, wantErr: "parser is required"},
|
||||
{name: "unknown parser", target: ProbeURLConfig{URL: "https://example.com", Parser: "ip_api"}, wantErr: "unsupported parser"},
|
||||
{name: "relative URL", target: ProbeURLConfig{URL: "/cdn-cgi/trace", Parser: "chatgpt-trace"}, wantErr: "invalid url"},
|
||||
{name: "unsupported scheme", target: ProbeURLConfig{URL: "ftp://example.com/file", Parser: "ipify"}, wantErr: "scheme must be http or https"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := normalizeProxyProbeURLs([]ProbeURLConfig{tt.target})
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// 渠道监控「配额模式」的归一化配额快照类型。
|
||||
//
|
||||
// 配额模式监控不直接对接上游,而是关联一个已有账号,复用账号侧的用量服务
|
||||
// (AccountUsageService / CNProviderQuotaService / CNProviderBalanceService),
|
||||
// 把各平台形态各异的用量数据归一成 MonitorQuotaSnapshot,随检测历史持久化
|
||||
// 到 channel_monitor_histories.quota(JSONB),供管理端与用户端渲染。
|
||||
//
|
||||
// 类型放在 domain 包是因为 ent schema(internal/domain 的下游)需要引用它做
|
||||
// field.JSON 序列化;service 不能被 ent import(会造成循环依赖)。
|
||||
|
||||
// MonitorQuotaTier 单个用量窗口的快照。
|
||||
//
|
||||
// Window 取值约定(与前端 monitorCommon.quota.windows.* 标签一一对应):
|
||||
// - "5h" 5 小时滚动窗口(Claude/Codex/Kimi/Zhipu coding plan)
|
||||
// - "7d" 7 天窗口(Claude/Codex)
|
||||
// - "7d-sonnet" Claude 7 天 Sonnet 独立额度
|
||||
// - "7d-fable" Claude 7 天 Fable 独立额度
|
||||
// - "weekly" 周窗口(Kimi/Zhipu coding plan)
|
||||
// - "daily" 日窗口(Gemini 日配额 / Grok 日请求)
|
||||
// - "30d" 30 天窗口(Grok 月度)
|
||||
// - "total" 无窗口语义的总量额度(Antigravity per-model 等)
|
||||
//
|
||||
// 同一 Window 可能出现多条(Gemini 多档日配额、Antigravity per-model、
|
||||
// Grok requests/tokens),用 Label 区分:Label 是机器 token(requests/tokens/
|
||||
// shared/pro/flash 或模型名),前端已知 token 走 i18n,未知原样展示。
|
||||
type MonitorQuotaTier struct {
|
||||
Window string `json:"window"`
|
||||
Label string `json:"label,omitempty"`
|
||||
UsedPercent float64 `json:"used_percent"` // 0-100+;仅有绝对值时按 used/limit 计算
|
||||
Used float64 `json:"used,omitempty"`
|
||||
Limit float64 `json:"limit,omitempty"`
|
||||
ResetAt string `json:"reset_at,omitempty"` // RFC3339;未知时留空
|
||||
}
|
||||
|
||||
// MonitorQuotaSnapshot 一次配额查询的完整快照。
|
||||
//
|
||||
// Source 取值:
|
||||
// - "usage" 海外平台(AccountUsageService.GetUsage)
|
||||
// - "cn_quota" 国产 Coding Plan(CNProviderQuotaService.QueryUsage)
|
||||
// - "cn_balance" 国产按量付费余额(CNProviderBalanceService.QueryBalance)
|
||||
type MonitorQuotaSnapshot struct {
|
||||
Source string `json:"source"`
|
||||
Success bool `json:"success"`
|
||||
Tiers []MonitorQuotaTier `json:"tiers,omitempty"`
|
||||
Balance *float64 `json:"balance,omitempty"` // cn_balance 主余额
|
||||
Balances []MonitorBalance `json:"balances,omitempty"` // 多币种余额(如 DeepSeek CNY+USD)
|
||||
Currency string `json:"currency,omitempty"` // 主余额币种
|
||||
PlanLevel string `json:"plan_level,omitempty"` // 套餐等级(如智谱 level)
|
||||
// BalanceLow 余额低于阈值或账号被上游标记不可用(仅 cn_balance 来源)。
|
||||
// 抓取器按 Gateway.CNProviders.BalanceThreshold 判定,口径与账号停调
|
||||
// (CNProviderBalanceCheckService.checkOne)一致:任一币种达标即健康。
|
||||
BalanceLow bool `json:"balance_low,omitempty"`
|
||||
// CredentialInvalid 上游 401/403 鉴权失败(区别于网络/解析错误),
|
||||
// 检测状态据此推导 failed 而非 error。
|
||||
CredentialInvalid bool `json:"credential_invalid,omitempty"`
|
||||
Error string `json:"error,omitempty"` // Success=false 时的错误摘要
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
// MonitorBalance 单币种余额条目。
|
||||
type MonitorBalance struct {
|
||||
Currency string `json:"currency"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
@@ -23,7 +23,28 @@ const (
|
||||
PlatformGemini = "gemini"
|
||||
PlatformAntigravity = "antigravity"
|
||||
PlatformGrok = "grok"
|
||||
PlatformComposite = "composite"
|
||||
// 国产 OpenAI 兼容供应商(经 OpenAI 网关转发,按 Chat Completions 协议)。
|
||||
PlatformKimi = "kimi" // Kimi (月之暗面 / Moonshot)
|
||||
PlatformZhipu = "zhipu" // 智谱 GLM (bigmodel)
|
||||
PlatformDeepseek = "deepseek" // DeepSeek
|
||||
PlatformComposite = "composite"
|
||||
)
|
||||
|
||||
// Account mode constants 区分国产供应商的「按量付费(余额)」与「Coding Plan」两种接入方式。
|
||||
// 存储于 credentials["account_mode"],决定 base_url 预设与额度监控方式。
|
||||
const (
|
||||
AccountModePayG = "payg" // 按量付费:消耗余额,做余额检测冷却
|
||||
AccountModeCoding = "coding" // Coding Plan:滚动用量窗口冷却(5h / weekly)
|
||||
)
|
||||
|
||||
// API protocol constants 国产供应商的上游 API 协议维度。存储于
|
||||
// credentials["api_protocol"],与 account_mode 正交:协议决定转发端点与格式,
|
||||
// 模式决定额度监控方式。同协议请求零转换直通;跨协议组合才走转换链。
|
||||
const (
|
||||
APIProtocolChatCompletions = "chat_completions" // OpenAI Chat Completions(默认)
|
||||
APIProtocolAnthropic = "anthropic" // 原生 Anthropic /v1/messages(适配 Claude Code)
|
||||
APIProtocolResponses = "responses" // OpenAI Responses(仅 deepseek,适配 Codex)
|
||||
APIProtocolAdaptive = "adaptive" // 按入站协议优先选择供应商原生端点
|
||||
)
|
||||
|
||||
// Account type constants
|
||||
@@ -117,6 +138,12 @@ var DefaultAntigravityModelMapping = map[string]string{
|
||||
"gemini-3.1-flash-image": "gemini-3.1-flash-image",
|
||||
// Gemini 3.1 image preview 映射
|
||||
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
|
||||
// Gemini 3.6 Flash tiered models
|
||||
"gemini-3.6-flash": "gemini-3.6-flash",
|
||||
"gemini-3.6-flash-high": "gemini-3.6-flash-high",
|
||||
"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 image 兼容映射(向 3.1 image 迁移)
|
||||
"gemini-3-pro-image": "gemini-3.1-flash-image",
|
||||
"gemini-3-pro-image-preview": "gemini-3.1-flash-image",
|
||||
|
||||
@@ -65,6 +65,14 @@ func TestDefaultAntigravityModelMapping_Gemini31ProAliases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAntigravityModelMapping_Gemini36FlashModels(t *testing.T) {
|
||||
for _, model := range []string{"gemini-3.6-flash", "gemini-3.6-flash-high", "gemini-3.6-flash-low", "gemini-3.6-flash-medium", "gemini-3.6-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()
|
||||
|
||||
|
||||
@@ -147,6 +147,8 @@ type UpdateAccountRequest struct {
|
||||
GroupIDs *[]int64 `json:"group_ids"`
|
||||
ExpiresAt *int64 `json:"expires_at"`
|
||||
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||
ProbeEnabled *bool `json:"upstream_billing_probe_enabled"`
|
||||
RateSyncEnabled *bool `json:"upstream_billing_rate_sync_enabled"`
|
||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||
}
|
||||
|
||||
@@ -987,6 +989,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
||||
GroupIDs: req.GroupIDs,
|
||||
ExpiresAt: req.ExpiresAt,
|
||||
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||
ProbeEnabled: req.ProbeEnabled,
|
||||
RateSyncEnabled: req.RateSyncEnabled,
|
||||
SkipMixedChannelCheck: skipCheck,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1021,7 +1025,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
||||
// 当前请求。探测错误仅记录日志,不向上下文传播:探测失败时标记保持缺失,
|
||||
// 网关会按"现状即证据"默认走 Responses。
|
||||
func (h *AccountHandler) scheduleOpenAIResponsesProbe(account *service.Account) {
|
||||
if account == nil || account.Platform != service.PlatformOpenAI || account.Type != service.AccountTypeAPIKey {
|
||||
if account == nil || account.Type != service.AccountTypeAPIKey ||
|
||||
(account.Platform != service.PlatformOpenAI && !service.IsCNProvider(account.Platform)) {
|
||||
return
|
||||
}
|
||||
if h.accountTestService == nil {
|
||||
@@ -1061,6 +1066,10 @@ type TestAccountRequest struct {
|
||||
ModelID string `json:"model_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Mode string `json:"mode"`
|
||||
// Optional media for Grok (and future) real generation tests.
|
||||
// ImageDataURL / AudioDataURL are data:<mime>;base64,... payloads.
|
||||
ImageDataURL string `json:"image_data_url"`
|
||||
AudioDataURL string `json:"audio_data_url"`
|
||||
}
|
||||
|
||||
type SyncFromCRSRequest struct {
|
||||
@@ -1090,8 +1099,13 @@ func (h *AccountHandler) Test(c *gin.Context) {
|
||||
// Allow empty body, model_id is optional
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
opts := service.AccountTestOptions{
|
||||
ImageDataURL: req.ImageDataURL,
|
||||
AudioDataURL: req.AudioDataURL,
|
||||
}
|
||||
|
||||
// Use AccountTestService to test the account with SSE streaming
|
||||
if err := h.accountTestService.TestAccountConnection(c, accountID, req.ModelID, req.Prompt, req.Mode); err != nil {
|
||||
if err := h.accountTestService.TestAccountConnection(c, accountID, req.ModelID, req.Prompt, req.Mode, opts); err != nil {
|
||||
// Error already sent via SSE, just log
|
||||
return
|
||||
}
|
||||
@@ -1411,6 +1425,9 @@ func (h *AccountHandler) ApplyOAuthCredentials(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Drop SSO/password residue; re-auth must leave only OAuth tokens on disk.
|
||||
req.Credentials = service.SanitizeStoredCredentials(existing.Platform, req.Credentials)
|
||||
|
||||
updatedAccount, err := h.adminService.UpdateAccount(ctx, accountID, &service.UpdateAccountInput{
|
||||
Type: req.Type,
|
||||
Credentials: req.Credentials,
|
||||
@@ -1438,6 +1455,20 @@ func (h *AccountHandler) ApplyOAuthCredentials(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Successful re-auth clears the soft spending-limit reauth flag for Grok.
|
||||
if existing.Platform == service.PlatformGrok {
|
||||
if clearErr := h.adminService.UpdateAccountExtra(ctx, accountID, map[string]any{
|
||||
"grok_needs_reauth": false,
|
||||
"grok_needs_reauth_reason": "",
|
||||
"grok_needs_reauth_at": "",
|
||||
}); clearErr != nil {
|
||||
slog.Warn("apply_oauth_credentials.clear_grok_reauth_failed",
|
||||
"account_id", accountID,
|
||||
"err", clearErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if cleared, clearErr := h.adminService.ClearAccountError(ctx, accountID); clearErr != nil {
|
||||
slog.Warn("apply_oauth_credentials.clear_error_failed",
|
||||
"account_id", accountID,
|
||||
@@ -1531,6 +1562,141 @@ func (h *AccountHandler) RevertProxyFallback(c *gin.Context) {
|
||||
response.Success(c, gin.H{"message": "reverted"})
|
||||
}
|
||||
|
||||
// BatchDelete handles deleting multiple accounts with bounded concurrency.
|
||||
// POST /api/v1/admin/accounts/batch-delete
|
||||
func (h *AccountHandler) BatchDelete(c *gin.Context) {
|
||||
var req struct {
|
||||
AccountIDs []int64 `json:"account_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
accountIDs := normalizeInt64IDList(req.AccountIDs)
|
||||
if len(accountIDs) == 0 {
|
||||
response.BadRequest(c, "account_ids is required")
|
||||
return
|
||||
}
|
||||
|
||||
accounts, err := h.adminService.GetAccountsByIDs(c.Request.Context(), accountIDs)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
type deleteError struct {
|
||||
AccountID int64 `json:"account_id"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
requestedIDs := make(map[int64]struct{}, len(accountIDs))
|
||||
for _, accountID := range accountIDs {
|
||||
requestedIDs[accountID] = struct{}{}
|
||||
}
|
||||
accountsByID := make(map[int64]*service.Account, len(accounts))
|
||||
for _, account := range accounts {
|
||||
if account != nil {
|
||||
accountsByID[account.ID] = account
|
||||
}
|
||||
}
|
||||
|
||||
rootIDs := make([]int64, 0, len(accountIDs))
|
||||
dependentIDs := make(map[int64][]int64)
|
||||
failedIDs := make([]int64, 0)
|
||||
errorsByAccount := make([]deleteError, 0)
|
||||
for _, accountID := range accountIDs {
|
||||
account := accountsByID[accountID]
|
||||
if account == nil {
|
||||
failedIDs = append(failedIDs, accountID)
|
||||
errorsByAccount = append(errorsByAccount, deleteError{
|
||||
AccountID: accountID,
|
||||
Error: "account not found",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
rootID := accountID
|
||||
visited := map[int64]struct{}{accountID: {}}
|
||||
for {
|
||||
current := accountsByID[rootID]
|
||||
if current == nil || current.ParentAccountID == nil {
|
||||
break
|
||||
}
|
||||
parentID := *current.ParentAccountID
|
||||
if _, selected := requestedIDs[parentID]; !selected {
|
||||
break
|
||||
}
|
||||
if _, exists := accountsByID[parentID]; !exists {
|
||||
break
|
||||
}
|
||||
if _, cyclic := visited[parentID]; cyclic {
|
||||
rootID = accountID
|
||||
break
|
||||
}
|
||||
visited[parentID] = struct{}{}
|
||||
rootID = parentID
|
||||
}
|
||||
|
||||
if rootID != accountID {
|
||||
dependentIDs[rootID] = append(dependentIDs[rootID], accountID)
|
||||
continue
|
||||
}
|
||||
rootIDs = append(rootIDs, accountID)
|
||||
}
|
||||
|
||||
const maxConcurrency = 5
|
||||
g, gctx := errgroup.WithContext(c.Request.Context())
|
||||
g.SetLimit(maxConcurrency)
|
||||
|
||||
var mu sync.Mutex
|
||||
successIDs := make([]int64, 0, len(accountIDs))
|
||||
|
||||
// Every worker returns nil so one account failure does not cancel the remaining deletions.
|
||||
for _, id := range rootIDs {
|
||||
accountID := id
|
||||
g.Go(func() error {
|
||||
err := h.adminService.DeleteAccount(gctx, accountID)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
affectedIDs := append([]int64{accountID}, dependentIDs[accountID]...)
|
||||
if err != nil {
|
||||
for _, affectedID := range affectedIDs {
|
||||
failedIDs = append(failedIDs, affectedID)
|
||||
errorsByAccount = append(errorsByAccount, deleteError{
|
||||
AccountID: affectedID,
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
successIDs = append(successIDs, affectedIDs...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(successIDs, func(i, j int) bool { return successIDs[i] < successIDs[j] })
|
||||
sort.Slice(failedIDs, func(i, j int) bool { return failedIDs[i] < failedIDs[j] })
|
||||
sort.Slice(errorsByAccount, func(i, j int) bool {
|
||||
return errorsByAccount[i].AccountID < errorsByAccount[j].AccountID
|
||||
})
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"total": len(accountIDs),
|
||||
"success": len(successIDs),
|
||||
"failed": len(failedIDs),
|
||||
"success_ids": successIDs,
|
||||
"failed_ids": failedIDs,
|
||||
"errors": errorsByAccount,
|
||||
})
|
||||
}
|
||||
|
||||
// BatchClearError handles batch clearing account errors
|
||||
// POST /api/v1/admin/accounts/batch-clear-error
|
||||
func (h *AccountHandler) BatchClearError(c *gin.Context) {
|
||||
@@ -2283,6 +2449,11 @@ type BatchTodayStatsRequest struct {
|
||||
AccountIDs []int64 `json:"account_ids" binding:"required"`
|
||||
}
|
||||
|
||||
type BatchUsageRequest struct {
|
||||
AccountIDs []int64 `json:"account_ids" binding:"required"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
// GetBatchTodayStats 批量获取多个账号的今日统计。
|
||||
// POST /api/v1/admin/accounts/today-stats/batch
|
||||
func (h *AccountHandler) GetBatchTodayStats(c *gin.Context) {
|
||||
@@ -2329,6 +2500,36 @@ func (h *AccountHandler) GetBatchTodayStats(c *gin.Context) {
|
||||
response.Success(c, payload)
|
||||
}
|
||||
|
||||
// GetBatchUsage 批量获取多个账号的 current usage。
|
||||
// POST /api/v1/admin/accounts/usage/batch
|
||||
func (h *AccountHandler) GetBatchUsage(c *gin.Context) {
|
||||
var req BatchUsageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
accountIDs := normalizeInt64IDList(req.AccountIDs)
|
||||
if len(accountIDs) == 0 {
|
||||
response.Success(c, gin.H{
|
||||
"usage": map[string]any{},
|
||||
"errors": map[string]string{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
usageByAccount, errorsByAccount, err := h.accountUsageService.GetUsageBatch(c.Request.Context(), accountIDs, req.Force)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"usage": usageByAccount,
|
||||
"errors": errorsByAccount,
|
||||
})
|
||||
}
|
||||
|
||||
// SetSchedulableRequest represents the request body for setting schedulable status
|
||||
type SetSchedulableRequest struct {
|
||||
Schedulable bool `json:"schedulable"`
|
||||
@@ -2413,8 +2614,14 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
|
||||
|
||||
// Handle Gemini accounts
|
||||
if account.IsGemini() {
|
||||
// For OAuth accounts: return default Gemini models
|
||||
// Consumer Google One OAuth still uses the legacy Gemini CLI / Code
|
||||
// Assist channel. Do not advertise newer 3.x or image models that the
|
||||
// channel cannot serve.
|
||||
if account.IsOAuth() {
|
||||
if account.IsGeminiGoogleOne() {
|
||||
response.Success(c, geminicli.GoogleOneModels)
|
||||
return
|
||||
}
|
||||
response.Success(c, geminicli.DefaultModels)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -287,6 +287,42 @@ func TestAccountHandlerGetAvailableModels_OpenAISparkShadowReturnsMappingModels(
|
||||
}, ids, "影子可用模型由 model_mapping 派生(非写死)")
|
||||
}
|
||||
|
||||
func TestAccountHandlerGetAvailableModels_GeminiGoogleOneUsesConservativeCatalog(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
account: service.Account{
|
||||
ID: 45,
|
||||
Name: "google-one",
|
||||
Platform: service.PlatformGemini,
|
||||
Type: service.AccountTypeOAuth,
|
||||
Status: service.StatusActive,
|
||||
Credentials: map[string]any{
|
||||
"oauth_type": "google_one",
|
||||
},
|
||||
},
|
||||
}
|
||||
router := setupAvailableModelsRouter(svc)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/45/models", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
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))
|
||||
ids := make([]string, 0, len(resp.Data))
|
||||
for _, model := range resp.Data {
|
||||
ids = append(ids, model.ID)
|
||||
}
|
||||
require.ElementsMatch(t, []string{"gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"}, ids)
|
||||
require.NotContains(t, ids, "gemini-3.5-flash")
|
||||
require.NotContains(t, ids, "gemini-2.5-flash-image")
|
||||
}
|
||||
|
||||
func TestAccountHandlerSyncUpstreamModels_ConfigErrorReturnsBadRequest(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
type batchDeleteAdminService struct {
|
||||
*stubAdminService
|
||||
|
||||
mu sync.Mutex
|
||||
active int
|
||||
maxActive int
|
||||
deletedIDs []int64
|
||||
deleteErrorsByID map[int64]error
|
||||
accountsByID map[int64]*service.Account
|
||||
}
|
||||
|
||||
func (s *batchDeleteAdminService) GetAccountsByIDs(_ context.Context, ids []int64) ([]*service.Account, error) {
|
||||
accounts := make([]*service.Account, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if s.accountsByID != nil {
|
||||
if account, ok := s.accountsByID[id]; ok {
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
continue
|
||||
}
|
||||
accounts = append(accounts, &service.Account{ID: id})
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (s *batchDeleteAdminService) DeleteAccount(ctx context.Context, id int64) error {
|
||||
s.mu.Lock()
|
||||
s.active++
|
||||
if s.active > s.maxActive {
|
||||
s.maxActive = s.active
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.mu.Lock()
|
||||
s.active--
|
||||
s.mu.Unlock()
|
||||
return ctx.Err()
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.active--
|
||||
s.deletedIDs = append(s.deletedIDs, id)
|
||||
return s.deleteErrorsByID[id]
|
||||
}
|
||||
|
||||
func setupAccountBatchDeleteRouter(adminSvc *batchDeleteAdminService) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
router.POST("/api/v1/admin/accounts/batch-delete", handler.BatchDelete)
|
||||
return router
|
||||
}
|
||||
|
||||
func TestAccountHandlerBatchDeleteReturnsStablePerAccountResults(t *testing.T) {
|
||||
adminSvc := &batchDeleteAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
deleteErrorsByID: map[int64]error{
|
||||
3: errors.New("delete failed"),
|
||||
},
|
||||
}
|
||||
router := setupAccountBatchDeleteRouter(adminSvc)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/v1/admin/accounts/batch-delete",
|
||||
bytes.NewBufferString(`{"account_ids":[5,4,3,2,1,2,0,-1]}`),
|
||||
)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var payload struct {
|
||||
Data struct {
|
||||
Total int `json:"total"`
|
||||
Success int `json:"success"`
|
||||
Failed int `json:"failed"`
|
||||
SuccessIDs []int64 `json:"success_ids"`
|
||||
FailedIDs []int64 `json:"failed_ids"`
|
||||
Errors []struct {
|
||||
AccountID int64 `json:"account_id"`
|
||||
Error string `json:"error"`
|
||||
} `json:"errors"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, 5, payload.Data.Total)
|
||||
require.Equal(t, 4, payload.Data.Success)
|
||||
require.Equal(t, 1, payload.Data.Failed)
|
||||
require.Equal(t, []int64{1, 2, 4, 5}, payload.Data.SuccessIDs)
|
||||
require.Equal(t, []int64{3}, payload.Data.FailedIDs)
|
||||
require.Equal(t, int64(3), payload.Data.Errors[0].AccountID)
|
||||
require.Equal(t, "delete failed", payload.Data.Errors[0].Error)
|
||||
require.LessOrEqual(t, adminSvc.maxActive, 5)
|
||||
require.Greater(t, adminSvc.maxActive, 1)
|
||||
}
|
||||
|
||||
func TestAccountHandlerBatchDeleteDoesNotRaceSelectedShadowWithParent(t *testing.T) {
|
||||
parentID := int64(1)
|
||||
adminSvc := &batchDeleteAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
accountsByID: map[int64]*service.Account{
|
||||
1: {ID: 1},
|
||||
2: {ID: 2, ParentAccountID: &parentID},
|
||||
3: {ID: 3},
|
||||
},
|
||||
}
|
||||
router := setupAccountBatchDeleteRouter(adminSvc)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/v1/admin/accounts/batch-delete",
|
||||
bytes.NewBufferString(`{"account_ids":[1,2,3]}`),
|
||||
)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var payload struct {
|
||||
Data struct {
|
||||
SuccessIDs []int64 `json:"success_ids"`
|
||||
FailedIDs []int64 `json:"failed_ids"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, []int64{1, 2, 3}, payload.Data.SuccessIDs)
|
||||
require.Empty(t, payload.Data.FailedIDs)
|
||||
require.ElementsMatch(t, []int64{1, 3}, adminSvc.deletedIDs)
|
||||
}
|
||||
|
||||
func TestAccountHandlerBatchDeleteRejectsEmptyNormalizedIDs(t *testing.T) {
|
||||
adminSvc := &batchDeleteAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
}
|
||||
router := setupAccountBatchDeleteRouter(adminSvc)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/v1/admin/accounts/batch-delete",
|
||||
bytes.NewBufferString(`{"account_ids":[0,-1]}`),
|
||||
)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func TestApplyOAuthCredentialsRejectsMalformedOpenAILongContextBillingBeforeMuta
|
||||
|
||||
func TestOpenAIOAuthCodexPATBoundaryRejectsMalformedOpenAILongContextBillingValueBeforeTokenValidation(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
handler := NewOpenAIOAuthHandler(nil, newStubAdminService(), nil)
|
||||
handler := NewOpenAIOAuthHandler(nil, newStubAdminService(), nil, nil)
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
router.POST("/openai/create-from-codex-pat", handler.CreateAccountFromCodexPAT)
|
||||
|
||||
@@ -147,6 +147,28 @@ func TestAccountHandlerUpdateMixedChannelConflictSimplifiedResponse(t *testing.T
|
||||
require.False(t, hasRequireConfirmation)
|
||||
}
|
||||
|
||||
func TestAccountHandlerUpdateMapsUpstreamBillingRateSyncSettings(t *testing.T) {
|
||||
adminSvc := newStubAdminService()
|
||||
router := setupAccountMixedChannelRouter(adminSvc)
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"name": "gemini-key",
|
||||
"upstream_billing_probe_enabled": true,
|
||||
"upstream_billing_rate_sync_enabled": true,
|
||||
})
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/accounts/42", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.NotNil(t, adminSvc.lastUpdateAccountInput)
|
||||
require.NotNil(t, adminSvc.lastUpdateAccountInput.ProbeEnabled)
|
||||
require.True(t, *adminSvc.lastUpdateAccountInput.ProbeEnabled)
|
||||
require.NotNil(t, adminSvc.lastUpdateAccountInput.RateSyncEnabled)
|
||||
require.True(t, *adminSvc.lastUpdateAccountInput.RateSyncEnabled)
|
||||
}
|
||||
|
||||
func TestAccountHandlerBulkUpdateMixedChannelConflict(t *testing.T) {
|
||||
adminSvc := newStubAdminService()
|
||||
adminSvc.bulkUpdateAccountErr = &service.MixedChannelError{
|
||||
|
||||
@@ -32,6 +32,7 @@ type stubAdminService struct {
|
||||
createAccountErr error
|
||||
createSparkShadowErr error
|
||||
updateAccountErr error
|
||||
lastUpdateAccountInput *service.UpdateAccountInput
|
||||
bulkUpdateAccountErr error
|
||||
lastBulkUpdateAccountInput *service.BulkUpdateAccountsInput
|
||||
getAccountResult *service.Account
|
||||
@@ -501,6 +502,7 @@ func (s *stubAdminService) RecoverDuplicateAccount(ctx context.Context, id int64
|
||||
|
||||
func (s *stubAdminService) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
|
||||
s.updateAccountCalls++
|
||||
s.lastUpdateAccountInput = input
|
||||
if s.updateAccountErr != nil {
|
||||
return nil, s.updateAccountErr
|
||||
}
|
||||
|
||||
@@ -153,12 +153,12 @@ func (h *BackupHandler) GetDownloadURL(c *gin.Context) {
|
||||
response.BadRequest(c, "backup ID is required")
|
||||
return
|
||||
}
|
||||
url, err := h.backupService.GetBackupDownloadURL(c.Request.Context(), backupID)
|
||||
download, err := h.backupService.GetBackupDownloadURL(c.Request.Context(), backupID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"url": url})
|
||||
response.Success(c, download)
|
||||
}
|
||||
|
||||
// ─── 恢复操作(需要重新输入管理员密码) ───
|
||||
|
||||
@@ -33,7 +33,7 @@ type createChannelRequest struct {
|
||||
GroupIDs []int64 `json:"group_ids"`
|
||||
ModelPricing []channelModelPricingRequest `json:"model_pricing"`
|
||||
ModelMapping map[string]map[string]string `json:"model_mapping"`
|
||||
BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"`
|
||||
BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped response_model"`
|
||||
RestrictModels bool `json:"restrict_models"`
|
||||
Features string `json:"features"`
|
||||
FeaturesConfig map[string]any `json:"features_config"`
|
||||
@@ -48,7 +48,7 @@ type updateChannelRequest struct {
|
||||
GroupIDs *[]int64 `json:"group_ids"`
|
||||
ModelPricing *[]channelModelPricingRequest `json:"model_pricing"`
|
||||
ModelMapping map[string]map[string]string `json:"model_mapping"`
|
||||
BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"`
|
||||
BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped response_model"`
|
||||
RestrictModels *bool `json:"restrict_models"`
|
||||
Features *string `json:"features"`
|
||||
FeaturesConfig map[string]any `json:"features_config"`
|
||||
@@ -57,29 +57,48 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type channelTimePricingRequest struct {
|
||||
Timezone string `json:"timezone"`
|
||||
WeekdaysOnly bool `json:"weekdays_only"`
|
||||
Periods []channelTimePricingPeriodRequest `json:"periods"`
|
||||
}
|
||||
|
||||
type channelTimePricingPeriodRequest struct {
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
}
|
||||
|
||||
type pricingIntervalRequest struct {
|
||||
MinTokens int `json:"min_tokens"`
|
||||
MaxTokens *int `json:"max_tokens"`
|
||||
TierLabel string `json:"tier_label"`
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
MinTokens int `json:"min_tokens"`
|
||||
MaxTokens *int `json:"max_tokens"`
|
||||
TierLabel string `json:"tier_label"`
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_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"`
|
||||
CacheWriteMultiplier *float64 `json:"cache_write_multiplier" binding:"omitempty,gt=0"`
|
||||
CacheReadMultiplier *float64 `json:"cache_read_multiplier" binding:"omitempty,gt=0"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
type accountStatsPricingRuleRequest struct {
|
||||
@@ -108,31 +127,50 @@ 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"`
|
||||
ImageInputPrice *float64 `json:"image_input_price"`
|
||||
ImageOutputPrice *float64 `json:"image_output_price"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
Intervals []pricingIntervalResponse `json:"intervals"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type channelTimePricingResponse struct {
|
||||
Timezone string `json:"timezone"`
|
||||
WeekdaysOnly bool `json:"weekdays_only"`
|
||||
Periods []channelTimePricingPeriodResponse `json:"periods"`
|
||||
}
|
||||
|
||||
type channelTimePricingPeriodResponse struct {
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
}
|
||||
|
||||
type pricingIntervalResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
MinTokens int `json:"min_tokens"`
|
||||
MaxTokens *int `json:"max_tokens"`
|
||||
TierLabel string `json:"tier_label,omitempty"`
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
ID int64 `json:"id"`
|
||||
MinTokens int `json:"min_tokens"`
|
||||
MaxTokens *int `json:"max_tokens"`
|
||||
TierLabel string `json:"tier_label,omitempty"`
|
||||
InputPrice *float64 `json:"input_price"`
|
||||
OutputPrice *float64 `json:"output_price"`
|
||||
CacheWritePrice *float64 `json:"cache_write_price"`
|
||||
CacheReadPrice *float64 `json:"cache_read_price"`
|
||||
InputMultiplier *float64 `json:"input_multiplier"`
|
||||
OutputMultiplier *float64 `json:"output_multiplier"`
|
||||
CacheWriteMultiplier *float64 `json:"cache_write_multiplier"`
|
||||
CacheReadMultiplier *float64 `json:"cache_read_multiplier"`
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
type accountStatsPricingRuleResponse struct {
|
||||
@@ -224,29 +262,55 @@ func pricingToResponse(p *service.ChannelModelPricing) channelModelPricingRespon
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
func timePricingToResponse(value *service.ChannelTimePricing) *channelTimePricingResponse {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
periods := make([]channelTimePricingPeriodResponse, 0, len(value.Periods))
|
||||
for _, period := range value.Periods {
|
||||
periods = append(periods, channelTimePricingPeriodResponse{
|
||||
StartTime: period.StartTime,
|
||||
EndTime: period.EndTime,
|
||||
Multiplier: period.Multiplier,
|
||||
})
|
||||
}
|
||||
return &channelTimePricingResponse{
|
||||
Timezone: value.Timezone,
|
||||
WeekdaysOnly: value.WeekdaysOnly,
|
||||
Periods: periods,
|
||||
}
|
||||
}
|
||||
|
||||
func intervalToResponse(iv service.PricingInterval) pricingIntervalResponse {
|
||||
return pricingIntervalResponse{
|
||||
ID: iv.ID,
|
||||
MinTokens: iv.MinTokens,
|
||||
MaxTokens: iv.MaxTokens,
|
||||
TierLabel: iv.TierLabel,
|
||||
InputPrice: iv.InputPrice,
|
||||
OutputPrice: iv.OutputPrice,
|
||||
CacheWritePrice: iv.CacheWritePrice,
|
||||
CacheReadPrice: iv.CacheReadPrice,
|
||||
PerRequestPrice: iv.PerRequestPrice,
|
||||
SortOrder: iv.SortOrder,
|
||||
ID: iv.ID,
|
||||
MinTokens: iv.MinTokens,
|
||||
MaxTokens: iv.MaxTokens,
|
||||
TierLabel: iv.TierLabel,
|
||||
InputPrice: iv.InputPrice,
|
||||
OutputPrice: iv.OutputPrice,
|
||||
CacheWritePrice: iv.CacheWritePrice,
|
||||
CacheReadPrice: iv.CacheReadPrice,
|
||||
InputMultiplier: iv.InputMultiplier,
|
||||
OutputMultiplier: iv.OutputMultiplier,
|
||||
CacheWriteMultiplier: iv.CacheWriteMultiplier,
|
||||
CacheReadMultiplier: iv.CacheReadMultiplier,
|
||||
PerRequestPrice: iv.PerRequestPrice,
|
||||
SortOrder: iv.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func pricingRequestToService(reqs []channelModelPricingRequest) []service.ChannelModelPricing {
|
||||
func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMultipliers bool) []service.ChannelModelPricing {
|
||||
result := make([]service.ChannelModelPricing, 0, len(reqs))
|
||||
for _, r := range reqs {
|
||||
billingMode := service.BillingMode(r.BillingMode)
|
||||
@@ -256,18 +320,34 @@ func pricingRequestToService(reqs []channelModelPricingRequest) []service.Channe
|
||||
platform := r.Platform
|
||||
intervals := make([]service.PricingInterval, 0, len(r.Intervals))
|
||||
for _, iv := range r.Intervals {
|
||||
var inputMultiplier, outputMultiplier, cacheWriteMultiplier, cacheReadMultiplier *float64
|
||||
if allowChannelMultipliers {
|
||||
inputMultiplier = iv.InputMultiplier
|
||||
outputMultiplier = iv.OutputMultiplier
|
||||
cacheWriteMultiplier = iv.CacheWriteMultiplier
|
||||
cacheReadMultiplier = iv.CacheReadMultiplier
|
||||
}
|
||||
intervals = append(intervals, service.PricingInterval{
|
||||
MinTokens: iv.MinTokens,
|
||||
MaxTokens: iv.MaxTokens,
|
||||
TierLabel: iv.TierLabel,
|
||||
InputPrice: iv.InputPrice,
|
||||
OutputPrice: iv.OutputPrice,
|
||||
CacheWritePrice: iv.CacheWritePrice,
|
||||
CacheReadPrice: iv.CacheReadPrice,
|
||||
PerRequestPrice: iv.PerRequestPrice,
|
||||
SortOrder: iv.SortOrder,
|
||||
MinTokens: iv.MinTokens,
|
||||
MaxTokens: iv.MaxTokens,
|
||||
TierLabel: iv.TierLabel,
|
||||
InputPrice: iv.InputPrice,
|
||||
OutputPrice: iv.OutputPrice,
|
||||
CacheWritePrice: iv.CacheWritePrice,
|
||||
CacheReadPrice: iv.CacheReadPrice,
|
||||
InputMultiplier: inputMultiplier,
|
||||
OutputMultiplier: outputMultiplier,
|
||||
CacheWriteMultiplier: cacheWriteMultiplier,
|
||||
CacheReadMultiplier: cacheReadMultiplier,
|
||||
PerRequestPrice: iv.PerRequestPrice,
|
||||
SortOrder: iv.SortOrder,
|
||||
})
|
||||
}
|
||||
var fastMultiplier, flexMultiplier *float64
|
||||
if allowChannelMultipliers {
|
||||
fastMultiplier = r.FastMultiplier
|
||||
flexMultiplier = r.FlexMultiplier
|
||||
}
|
||||
result = append(result, service.ChannelModelPricing{
|
||||
Platform: platform,
|
||||
Models: r.Models,
|
||||
@@ -276,21 +356,43 @@ func pricingRequestToService(reqs []channelModelPricingRequest) []service.Channe
|
||||
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),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func timePricingRequestToService(value *channelTimePricingRequest) *service.ChannelTimePricing {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
periods := make([]service.ChannelTimePricingPeriod, 0, len(value.Periods))
|
||||
for _, period := range value.Periods {
|
||||
periods = append(periods, service.ChannelTimePricingPeriod{
|
||||
StartTime: period.StartTime,
|
||||
EndTime: period.EndTime,
|
||||
Multiplier: period.Multiplier,
|
||||
})
|
||||
}
|
||||
return &service.ChannelTimePricing{
|
||||
Timezone: value.Timezone,
|
||||
WeekdaysOnly: value.WeekdaysOnly,
|
||||
Periods: periods,
|
||||
}
|
||||
}
|
||||
|
||||
func accountStatsPricingRuleRequestToService(r accountStatsPricingRuleRequest) service.AccountStatsPricingRule {
|
||||
return service.AccountStatsPricingRule{
|
||||
Name: r.Name,
|
||||
GroupIDs: r.GroupIDs,
|
||||
AccountIDs: r.AccountIDs,
|
||||
Pricing: pricingRequestToService(r.Pricing),
|
||||
Pricing: pricingRequestToService(r.Pricing, false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +453,7 @@ func (h *ChannelHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pricing := pricingRequestToService(req.ModelPricing)
|
||||
pricing := pricingRequestToService(req.ModelPricing, true)
|
||||
// Main model_pricing requires a platform; default to anthropic for backward compatibility.
|
||||
for i := range pricing {
|
||||
if pricing[i].Platform == "" {
|
||||
@@ -425,7 +527,7 @@ func (h *ChannelHandler) Update(c *gin.Context) {
|
||||
ApplyPricingToAccountStats: req.ApplyPricingToAccountStats,
|
||||
}
|
||||
if req.ModelPricing != nil {
|
||||
pricing := pricingRequestToService(*req.ModelPricing)
|
||||
pricing := pricingRequestToService(*req.ModelPricing, true)
|
||||
for i := range pricing {
|
||||
if pricing[i].Platform == "" {
|
||||
pricing[i].Platform = service.PlatformAnthropic
|
||||
@@ -512,9 +614,12 @@ func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) {
|
||||
var platformToLiteLLMProvider = map[string]string{
|
||||
service.PlatformAnthropic: "anthropic",
|
||||
service.PlatformOpenAI: "openai",
|
||||
service.PlatformGemini: "google",
|
||||
service.PlatformGemini: "gemini",
|
||||
service.PlatformAntigravity: "anthropic",
|
||||
service.PlatformGrok: "xai",
|
||||
service.PlatformKimi: "moonshot",
|
||||
service.PlatformZhipu: "zhipu",
|
||||
service.PlatformDeepseek: "deepseek",
|
||||
}
|
||||
|
||||
// SyncPricingModels 返回 LiteLLM 定价目录中指定平台的最新模型列表
|
||||
|
||||
@@ -305,7 +305,7 @@ func TestPricingRequestToService_Defaults(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := pricingRequestToService([]channelModelPricingRequest{tt.req})
|
||||
result := pricingRequestToService([]channelModelPricingRequest{tt.req}, true)
|
||||
require.Len(t, result, 1)
|
||||
switch tt.wantField {
|
||||
case "BillingMode":
|
||||
@@ -332,7 +332,7 @@ func TestPricingRequestToService_WithAllFields(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := pricingRequestToService(reqs)
|
||||
result := pricingRequestToService(reqs, true)
|
||||
require.Len(t, result, 1)
|
||||
r := result[0]
|
||||
require.Equal(t, "openai", r.Platform)
|
||||
@@ -373,7 +373,7 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := pricingRequestToService(reqs)
|
||||
result := pricingRequestToService(reqs, true)
|
||||
require.Len(t, result, 1)
|
||||
require.Len(t, result[0].Intervals, 2)
|
||||
|
||||
@@ -396,7 +396,7 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPricingRequestToService_EmptySlice(t *testing.T) {
|
||||
result := pricingRequestToService([]channelModelPricingRequest{})
|
||||
result := pricingRequestToService([]channelModelPricingRequest{}, true)
|
||||
require.NotNil(t, result)
|
||||
require.Empty(t, result)
|
||||
}
|
||||
@@ -410,7 +410,7 @@ func TestPricingRequestToService_NilPriceFields(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := pricingRequestToService(reqs)
|
||||
result := pricingRequestToService(reqs, true)
|
||||
require.Len(t, result, 1)
|
||||
r := result[0]
|
||||
require.Nil(t, r.InputPrice)
|
||||
@@ -421,6 +421,89 @@ func TestPricingRequestToService_NilPriceFields(t *testing.T) {
|
||||
require.Nil(t, r.PerRequestPrice)
|
||||
}
|
||||
|
||||
func TestPricingRequestToService_TimePricing(t *testing.T) {
|
||||
req := channelModelPricingRequest{
|
||||
Models: []string{"gpt-5"},
|
||||
BillingMode: "token",
|
||||
TimePricing: &channelTimePricingRequest{
|
||||
Timezone: "Asia/Shanghai",
|
||||
WeekdaysOnly: true,
|
||||
Periods: []channelTimePricingPeriodRequest{{
|
||||
StartTime: "09:00", EndTime: "12:00", Multiplier: 2,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
got := pricingRequestToService([]channelModelPricingRequest{req}, true)
|
||||
require.Equal(t, "Asia/Shanghai", got[0].TimePricing.Timezone)
|
||||
require.True(t, got[0].TimePricing.WeekdaysOnly)
|
||||
require.Equal(t, 2.0, got[0].TimePricing.Periods[0].Multiplier)
|
||||
}
|
||||
|
||||
func TestPricingRequestToService_TimePricingNil(t *testing.T) {
|
||||
got := pricingRequestToService([]channelModelPricingRequest{{Models: []string{"gpt-5"}}}, true)
|
||||
require.Nil(t, got[0].TimePricing)
|
||||
}
|
||||
|
||||
// 账号成本统计规则不支持倍率:allowChannelMultipliers=false 时必须丢弃,
|
||||
// 避免渠道倍率意外污染账号成本口径。
|
||||
func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) {
|
||||
req := channelModelPricingRequest{
|
||||
Models: []string{"gpt-5"},
|
||||
BillingMode: "token",
|
||||
FastMultiplier: float64Ptr(2.5),
|
||||
FlexMultiplier: float64Ptr(0.5),
|
||||
Intervals: []pricingIntervalRequest{{
|
||||
MinTokens: 272000,
|
||||
InputMultiplier: float64Ptr(2),
|
||||
OutputMultiplier: float64Ptr(1.5),
|
||||
CacheWriteMultiplier: float64Ptr(2),
|
||||
CacheReadMultiplier: float64Ptr(2),
|
||||
}},
|
||||
}
|
||||
|
||||
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, 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)
|
||||
require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].CacheReadMultiplier)
|
||||
|
||||
dropped := pricingRequestToService([]channelModelPricingRequest{req}, false)
|
||||
require.Nil(t, dropped[0].FastMultiplier)
|
||||
require.Nil(t, dropped[0].FlexMultiplier)
|
||||
require.Nil(t, dropped[0].Intervals[0].InputMultiplier)
|
||||
require.Nil(t, dropped[0].Intervals[0].OutputMultiplier)
|
||||
require.Nil(t, dropped[0].Intervals[0].CacheWriteMultiplier)
|
||||
require.Nil(t, dropped[0].Intervals[0].CacheReadMultiplier)
|
||||
// 非倍率字段不受开关影响
|
||||
require.Equal(t, 272000, dropped[0].Intervals[0].MinTokens)
|
||||
}
|
||||
|
||||
func TestPricingToResponse_TimePricing(t *testing.T) {
|
||||
got := pricingToResponse(&service.ChannelModelPricing{
|
||||
BillingMode: service.BillingModeToken,
|
||||
TimePricing: &service.ChannelTimePricing{
|
||||
Timezone: "Asia/Shanghai",
|
||||
WeekdaysOnly: true,
|
||||
Periods: []service.ChannelTimePricingPeriod{{
|
||||
StartTime: "14:00", EndTime: "18:00", Multiplier: 1.25,
|
||||
}},
|
||||
},
|
||||
})
|
||||
|
||||
require.NotNil(t, got.TimePricing)
|
||||
require.Equal(t, "Asia/Shanghai", got.TimePricing.Timezone)
|
||||
require.True(t, got.TimePricing.WeekdaysOnly)
|
||||
require.Equal(t, 1.25, got.TimePricing.Periods[0].Multiplier)
|
||||
}
|
||||
|
||||
func TestPricingToResponse_TimePricingNil(t *testing.T) {
|
||||
got := pricingToResponse(&service.ChannelModelPricing{})
|
||||
require.Nil(t, got.TimePricing)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. SyncPricingModels handler
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -459,7 +542,7 @@ func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) {
|
||||
svc := service.NewPricingService(nil, nil)
|
||||
router := setupSyncPricingModelsRouter(svc)
|
||||
|
||||
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity"} {
|
||||
for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity", "grok", "kimi", "zhipu", "deepseek"} {
|
||||
req := httptest.NewRequest(http.MethodGet, "/channels/pricing/sync-models?platform="+platform, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
@@ -39,10 +40,10 @@ 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"`
|
||||
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek"`
|
||||
APIMode string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"`
|
||||
Endpoint string `json:"endpoint" binding:"required,max=500"`
|
||||
APIKey string `json:"api_key" binding:"required,max=2000"`
|
||||
Endpoint string `json:"endpoint" binding:"omitempty,max=500"`
|
||||
APIKey string `json:"api_key" binding:"omitempty,max=2000"`
|
||||
PrimaryModel string `json:"primary_model" binding:"max=200"`
|
||||
ExtraModels []string `json:"extra_models"`
|
||||
GroupName string `json:"group_name" binding:"max=100"`
|
||||
@@ -53,11 +54,17 @@ type channelMonitorCreateRequest struct {
|
||||
ExtraHeaders map[string]string `json:"extra_headers"`
|
||||
BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
|
||||
BodyOverride map[string]any `json:"body_override"`
|
||||
|
||||
// CheckMode: probe(默认)/ quota / quota_probe。quota 模式 endpoint/api_key
|
||||
// 可空(条件必填校验在 service 层按模式分支)。
|
||||
CheckMode string `json:"check_mode" binding:"omitempty,oneof=probe quota quota_probe"`
|
||||
// AccountID: 配额模式关联的账号 ID。
|
||||
AccountID *int64 `json:"account_id"`
|
||||
}
|
||||
|
||||
type channelMonitorUpdateRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok"`
|
||||
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini grok antigravity kimi zhipu deepseek"`
|
||||
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"`
|
||||
@@ -72,6 +79,10 @@ type channelMonitorUpdateRequest struct {
|
||||
ExtraHeaders *map[string]string `json:"extra_headers"`
|
||||
BodyOverrideMode *string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
|
||||
BodyOverride *map[string]any `json:"body_override"`
|
||||
|
||||
// CheckMode/AccountID:nil = 不更新;AccountID 指向 0 = 清空关联。
|
||||
CheckMode *string `json:"check_mode" binding:"omitempty,oneof=probe quota quota_probe"`
|
||||
AccountID *int64 `json:"account_id"`
|
||||
}
|
||||
|
||||
type channelMonitorResponse struct {
|
||||
@@ -101,25 +112,33 @@ type channelMonitorResponse struct {
|
||||
ExtraHeaders map[string]string `json:"extra_headers"`
|
||||
BodyOverrideMode string `json:"body_override_mode"`
|
||||
BodyOverride map[string]any `json:"body_override"`
|
||||
|
||||
// 配额模式:check_mode + 关联账号 + 主模型最近配额快照
|
||||
// (LatestQuota 由 List handler 批量聚合后填充;管理端不受 channel_monitor_show_quota 影响)。
|
||||
CheckMode string `json:"check_mode"`
|
||||
AccountID *int64 `json:"account_id"`
|
||||
LatestQuota *domain.MonitorQuotaSnapshot `json:"latest_quota,omitempty"`
|
||||
}
|
||||
|
||||
type channelMonitorCheckResultResponse struct {
|
||||
Model string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
LatencyMs *int `json:"latency_ms"`
|
||||
PingLatencyMs *int `json:"ping_latency_ms"`
|
||||
Message string `json:"message"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
Model string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
LatencyMs *int `json:"latency_ms"`
|
||||
PingLatencyMs *int `json:"ping_latency_ms"`
|
||||
Message string `json:"message"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
Quota *domain.MonitorQuotaSnapshot `json:"quota,omitempty"`
|
||||
}
|
||||
|
||||
type channelMonitorHistoryItemResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
LatencyMs *int `json:"latency_ms"`
|
||||
PingLatencyMs *int `json:"ping_latency_ms"`
|
||||
Message string `json:"message"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
ID int64 `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
LatencyMs *int `json:"latency_ms"`
|
||||
PingLatencyMs *int `json:"ping_latency_ms"`
|
||||
Message string `json:"message"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
Quota *domain.MonitorQuotaSnapshot `json:"quota,omitempty"`
|
||||
}
|
||||
|
||||
// maskAPIKey 对 API Key 明文做脱敏:前 4 字符 + "***",长度 ≤ 4 时只显示 "***"。
|
||||
@@ -163,7 +182,10 @@ func channelMonitorToResponse(m *service.ChannelMonitor) *channelMonitorResponse
|
||||
ExtraHeaders: headers,
|
||||
BodyOverrideMode: m.BodyOverrideMode,
|
||||
BodyOverride: m.BodyOverride,
|
||||
// PrimaryStatus / PrimaryLatencyMs / Availability7d 由 List handler 在批量聚合后填充。
|
||||
CheckMode: m.CheckMode,
|
||||
AccountID: m.AccountID,
|
||||
// PrimaryStatus / PrimaryLatencyMs / Availability7d / LatestQuota
|
||||
// 由 List handler 在批量聚合后填充。
|
||||
}
|
||||
if m.LastCheckedAt != nil {
|
||||
s := m.LastCheckedAt.UTC().Format(time.RFC3339)
|
||||
@@ -180,6 +202,7 @@ func checkResultToResponse(r *service.CheckResult) channelMonitorCheckResultResp
|
||||
PingLatencyMs: r.PingLatencyMs,
|
||||
Message: r.Message,
|
||||
CheckedAt: r.CheckedAt.UTC().Format(time.RFC3339),
|
||||
Quota: r.Quota,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +215,7 @@ func historyEntryToResponse(e *service.ChannelMonitorHistoryEntry) channelMonito
|
||||
PingLatencyMs: e.PingLatencyMs,
|
||||
Message: e.Message,
|
||||
CheckedAt: e.CheckedAt.UTC().Format(time.RFC3339),
|
||||
Quota: e.Quota,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +294,7 @@ func buildListItemResponse(m *service.ChannelMonitor, summary service.MonitorSta
|
||||
resp.PrimaryStatus = summary.PrimaryStatus
|
||||
resp.PrimaryLatencyMs = summary.PrimaryLatencyMs
|
||||
resp.Availability7d = summary.Availability7d
|
||||
resp.LatestQuota = summary.LatestQuota
|
||||
resp.ExtraModelsStatus = make([]dto.ChannelMonitorExtraModelStatus, 0, len(summary.ExtraModels))
|
||||
for _, e := range summary.ExtraModels {
|
||||
resp.ExtraModelsStatus = append(resp.ExtraModelsStatus, dto.ChannelMonitorExtraModelStatus{
|
||||
@@ -327,6 +352,8 @@ func (h *ChannelMonitorHandler) Create(c *gin.Context) {
|
||||
ExtraHeaders: req.ExtraHeaders,
|
||||
BodyOverrideMode: req.BodyOverrideMode,
|
||||
BodyOverride: req.BodyOverride,
|
||||
CheckMode: req.CheckMode,
|
||||
AccountID: req.AccountID,
|
||||
})
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
@@ -421,6 +448,8 @@ func (h *ChannelMonitorHandler) Update(c *gin.Context) {
|
||||
ExtraHeaders: req.ExtraHeaders,
|
||||
BodyOverrideMode: req.BodyOverrideMode,
|
||||
BodyOverride: req.BodyOverride,
|
||||
CheckMode: req.CheckMode,
|
||||
AccountID: req.AccountID,
|
||||
})
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CNProviderHandler 暴露国产供应商(kimi/zhipu/deepseek)的额度与余额查询端点。
|
||||
//
|
||||
// - GET /admin/cn-providers/accounts/:id/quota Coding Plan 滚动窗口用量(kimi/zhipu)
|
||||
// - GET /admin/cn-providers/accounts/:id/balance payg 账号余额(kimi/deepseek)
|
||||
//
|
||||
// 智谱(zhipu)无余额端点,故同一账号仅 quota 或 balance 其一可用:服务端按账号
|
||||
// platform + account_mode 校验并返回明确错误(见 CNProvider*Service 的 load*Account)。
|
||||
type CNProviderHandler struct {
|
||||
quotaService *service.CNProviderQuotaService
|
||||
balanceService *service.CNProviderBalanceService
|
||||
}
|
||||
|
||||
func NewCNProviderHandler(
|
||||
quotaService *service.CNProviderQuotaService,
|
||||
balanceService *service.CNProviderBalanceService,
|
||||
) *CNProviderHandler {
|
||||
return &CNProviderHandler{
|
||||
quotaService: quotaService,
|
||||
balanceService: balanceService,
|
||||
}
|
||||
}
|
||||
|
||||
// QueryQuota 查询 Coding Plan 滚动窗口用量(5h + weekly)。
|
||||
func (h *CNProviderHandler) QueryQuota(c *gin.Context) {
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return
|
||||
}
|
||||
if h == nil || h.quotaService == nil {
|
||||
response.BadRequest(c, "cn provider quota service is not enabled")
|
||||
return
|
||||
}
|
||||
result, err := h.quotaService.QueryUsage(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// QueryBalance 查询 payg 账号余额。
|
||||
func (h *CNProviderHandler) QueryBalance(c *gin.Context) {
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return
|
||||
}
|
||||
if h == nil || h.balanceService == nil {
|
||||
response.BadRequest(c, "cn provider balance service is not enabled")
|
||||
return
|
||||
}
|
||||
result, err := h.balanceService.QueryBalance(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
@@ -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"`
|
||||
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"`
|
||||
APIKeys *[]string `json:"api_keys"`
|
||||
APIKeysMode string `json:"api_keys_mode"`
|
||||
@@ -60,6 +62,7 @@ type contentModerationAPIKeyTestRequest struct {
|
||||
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"`
|
||||
}
|
||||
@@ -88,6 +91,7 @@ func (h *ContentModerationHandler) UpdateConfig(c *gin.Context) {
|
||||
Mode: req.Mode,
|
||||
BaseURL: req.BaseURL,
|
||||
Model: req.Model,
|
||||
ProxyID: req.ProxyID,
|
||||
APIKey: req.APIKey,
|
||||
APIKeys: req.APIKeys,
|
||||
APIKeysMode: req.APIKeysMode,
|
||||
@@ -134,6 +138,7 @@ func (h *ContentModerationHandler) TestAPIKeys(c *gin.Context) {
|
||||
BaseURL: req.BaseURL,
|
||||
Model: req.Model,
|
||||
TimeoutMS: req.TimeoutMS,
|
||||
ProxyID: req.ProxyID,
|
||||
Prompt: req.Prompt,
|
||||
Images: req.Images,
|
||||
})
|
||||
|
||||
@@ -64,6 +64,18 @@ func parseTimeRange(c *gin.Context) (time.Time, time.Time) {
|
||||
return startTime, endTime
|
||||
}
|
||||
|
||||
func parseOptionalBoolDashboardFilter(c *gin.Context, name string) (*bool, error) {
|
||||
raw := strings.TrimSpace(c.Query(name))
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
value, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
// GetStats handles getting dashboard statistics
|
||||
// GET /api/v1/admin/dashboard/stats
|
||||
func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
@@ -200,6 +212,7 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
|
||||
var requestType *int16
|
||||
var stream *bool
|
||||
var billingType *int8
|
||||
var upstreamModelMismatch *bool
|
||||
|
||||
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||
if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil {
|
||||
@@ -249,8 +262,13 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
|
||||
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)
|
||||
trend, hit, err := h.getUsageTrendCached(c.Request.Context(), startTime, endTime, granularity, userID, apiKeyID, accountID, groupID, model, requestType, stream, billingType, upstreamModelMismatch)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "Failed to get usage trend")
|
||||
return
|
||||
@@ -277,6 +295,7 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
|
||||
var requestType *int16
|
||||
var stream *bool
|
||||
var billingType *int8
|
||||
var upstreamModelMismatch *bool
|
||||
|
||||
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||
if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil {
|
||||
@@ -330,8 +349,13 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
|
||||
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)
|
||||
stats, hit, err := h.getModelStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, modelSource, requestType, stream, billingType, upstreamModelMismatch)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "Failed to get model statistics")
|
||||
return
|
||||
@@ -355,6 +379,7 @@ func (h *DashboardHandler) GetGroupStats(c *gin.Context) {
|
||||
var requestType *int16
|
||||
var stream *bool
|
||||
var billingType *int8
|
||||
var upstreamModelMismatch *bool
|
||||
|
||||
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||
if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil {
|
||||
@@ -401,8 +426,13 @@ func (h *DashboardHandler) GetGroupStats(c *gin.Context) {
|
||||
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)
|
||||
stats, hit, err := h.getGroupStatsCached(c.Request.Context(), startTime, endTime, userID, apiKeyID, accountID, groupID, requestType, stream, billingType, upstreamModelMismatch)
|
||||
if err != nil {
|
||||
response.Error(c, 500, "Failed to get group statistics")
|
||||
return
|
||||
|
||||
@@ -19,11 +19,26 @@ type dashboardUsageRepoCapture struct {
|
||||
trendStream *bool
|
||||
modelRequestType *int16
|
||||
modelStream *bool
|
||||
trendMismatch *bool
|
||||
modelMismatch *bool
|
||||
groupMismatch *bool
|
||||
rankingLimit int
|
||||
ranking []usagestats.UserSpendingRankingItem
|
||||
rankingTotal float64
|
||||
}
|
||||
|
||||
func (s *dashboardUsageRepoCapture) GetUsageTrendWithUsageFilters(
|
||||
ctx context.Context,
|
||||
startTime, endTime time.Time,
|
||||
granularity string,
|
||||
filters usagestats.UsageLogFilters,
|
||||
) ([]usagestats.TrendDataPoint, error) {
|
||||
s.trendRequestType = filters.RequestType
|
||||
s.trendStream = filters.Stream
|
||||
s.trendMismatch = filters.UpstreamModelMismatch
|
||||
return []usagestats.TrendDataPoint{}, nil
|
||||
}
|
||||
|
||||
func (s *dashboardUsageRepoCapture) GetUsageTrendWithFilters(
|
||||
ctx context.Context,
|
||||
startTime, endTime time.Time,
|
||||
@@ -39,6 +54,27 @@ func (s *dashboardUsageRepoCapture) GetUsageTrendWithFilters(
|
||||
return []usagestats.TrendDataPoint{}, nil
|
||||
}
|
||||
|
||||
func (s *dashboardUsageRepoCapture) GetModelStatsWithUsageFiltersBySource(
|
||||
ctx context.Context,
|
||||
startTime, endTime time.Time,
|
||||
filters usagestats.UsageLogFilters,
|
||||
source string,
|
||||
) ([]usagestats.ModelStat, error) {
|
||||
s.modelRequestType = filters.RequestType
|
||||
s.modelStream = filters.Stream
|
||||
s.modelMismatch = filters.UpstreamModelMismatch
|
||||
return []usagestats.ModelStat{}, nil
|
||||
}
|
||||
|
||||
func (s *dashboardUsageRepoCapture) GetGroupStatsWithUsageFilters(
|
||||
ctx context.Context,
|
||||
startTime, endTime time.Time,
|
||||
filters usagestats.UsageLogFilters,
|
||||
) ([]usagestats.GroupStat, error) {
|
||||
s.groupMismatch = filters.UpstreamModelMismatch
|
||||
return []usagestats.GroupStat{}, nil
|
||||
}
|
||||
|
||||
func (s *dashboardUsageRepoCapture) GetModelStatsWithFilters(
|
||||
ctx context.Context,
|
||||
startTime, endTime time.Time,
|
||||
@@ -73,6 +109,7 @@ func newDashboardRequestTypeTestRouter(repo *dashboardUsageRepoCapture) *gin.Eng
|
||||
router := gin.New()
|
||||
router.GET("/admin/dashboard/trend", handler.GetUsageTrend)
|
||||
router.GET("/admin/dashboard/models", handler.GetModelStats)
|
||||
router.GET("/admin/dashboard/groups", handler.GetGroupStats)
|
||||
router.GET("/admin/dashboard/users-ranking", handler.GetUserSpendingRanking)
|
||||
return router
|
||||
}
|
||||
@@ -171,6 +208,46 @@ func TestDashboardModelStatsValidModelSource(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
func TestDashboardModelAuditFilterPropagatesToTrendModelAndGroupQueries(t *testing.T) {
|
||||
resetDashboardReadCachesForTest()
|
||||
repo := &dashboardUsageRepoCapture{}
|
||||
router := newDashboardRequestTypeTestRouter(repo)
|
||||
|
||||
for _, path := range []string{
|
||||
"/admin/dashboard/trend?upstream_model_mismatch=true",
|
||||
"/admin/dashboard/models?upstream_model_mismatch=true",
|
||||
"/admin/dashboard/groups?upstream_model_mismatch=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.trendMismatch)
|
||||
require.True(t, *repo.trendMismatch)
|
||||
require.NotNil(t, repo.modelMismatch)
|
||||
require.True(t, *repo.modelMismatch)
|
||||
require.NotNil(t, repo.groupMismatch)
|
||||
require.True(t, *repo.groupMismatch)
|
||||
}
|
||||
|
||||
func TestDashboardModelAuditFilterRejectsInvalidBoolean(t *testing.T) {
|
||||
repo := &dashboardUsageRepoCapture{}
|
||||
router := newDashboardRequestTypeTestRouter(repo)
|
||||
|
||||
for _, path := range []string{
|
||||
"/admin/dashboard/trend?upstream_model_mismatch=invalid",
|
||||
"/admin/dashboard/models?upstream_model_mismatch=invalid",
|
||||
"/admin/dashboard/groups?upstream_model_mismatch=invalid",
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardUsersRankingLimitAndCache(t *testing.T) {
|
||||
dashboardUsersRankingCache = newSnapshotCache(5 * time.Minute)
|
||||
repo := &dashboardUsageRepoCapture{
|
||||
|
||||
@@ -18,30 +18,32 @@ var (
|
||||
)
|
||||
|
||||
type dashboardTrendCacheKey struct {
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Granularity string `json:"granularity"`
|
||||
UserID int64 `json:"user_id"`
|
||||
APIKeyID int64 `json:"api_key_id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
Model string `json:"model"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Granularity string `json:"granularity"`
|
||||
UserID int64 `json:"user_id"`
|
||||
APIKeyID int64 `json:"api_key_id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
Model string `json:"model"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
|
||||
}
|
||||
|
||||
type dashboardModelGroupCacheKey struct {
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
UserID int64 `json:"user_id"`
|
||||
APIKeyID int64 `json:"api_key_id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
ModelSource string `json:"model_source,omitempty"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
UserID int64 `json:"user_id"`
|
||||
APIKeyID int64 `json:"api_key_id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
ModelSource string `json:"model_source,omitempty"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
|
||||
}
|
||||
|
||||
type dashboardEntityTrendCacheKey struct {
|
||||
@@ -84,22 +86,28 @@ func (h *DashboardHandler) getUsageTrendCached(
|
||||
requestType *int16,
|
||||
stream *bool,
|
||||
billingType *int8,
|
||||
upstreamModelMismatch *bool,
|
||||
) ([]usagestats.TrendDataPoint, bool, error) {
|
||||
key := mustMarshalDashboardCacheKey(dashboardTrendCacheKey{
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
Granularity: granularity,
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Model: model,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
Granularity: granularity,
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Model: model,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
entry, hit, err := dashboardTrendCache.GetOrLoad(key, func() (any, error) {
|
||||
return h.dashboardService.GetUsageTrendWithFilters(ctx, startTime, endTime, granularity, userID, apiKeyID, accountID, groupID, model, requestType, stream, billingType)
|
||||
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,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, hit, err
|
||||
@@ -116,21 +124,27 @@ func (h *DashboardHandler) getModelStatsCached(
|
||||
requestType *int16,
|
||||
stream *bool,
|
||||
billingType *int8,
|
||||
upstreamModelMismatch *bool,
|
||||
) ([]usagestats.ModelStat, bool, error) {
|
||||
key := mustMarshalDashboardCacheKey(dashboardModelGroupCacheKey{
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
ModelSource: usagestats.NormalizeModelSource(modelSource),
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
ModelSource: usagestats.NormalizeModelSource(modelSource),
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
entry, hit, err := dashboardModelStatsCache.GetOrLoad(key, func() (any, error) {
|
||||
return h.dashboardService.GetModelStatsWithFiltersBySource(ctx, startTime, endTime, userID, apiKeyID, accountID, groupID, requestType, stream, billingType, modelSource)
|
||||
return h.dashboardService.GetModelStatsWithUsageFiltersBySource(ctx, startTime, endTime, usagestats.UsageLogFilters{
|
||||
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
|
||||
RequestType: requestType, Stream: stream, BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
}, modelSource)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, hit, err
|
||||
@@ -146,20 +160,26 @@ func (h *DashboardHandler) getGroupStatsCached(
|
||||
requestType *int16,
|
||||
stream *bool,
|
||||
billingType *int8,
|
||||
upstreamModelMismatch *bool,
|
||||
) ([]usagestats.GroupStat, bool, error) {
|
||||
key := mustMarshalDashboardCacheKey(dashboardModelGroupCacheKey{
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
entry, hit, err := dashboardGroupStatsCache.GetOrLoad(key, func() (any, error) {
|
||||
return h.dashboardService.GetGroupStatsWithFilters(ctx, startTime, endTime, userID, apiKeyID, accountID, groupID, requestType, stream, billingType)
|
||||
return h.dashboardService.GetGroupStatsWithUsageFilters(ctx, startTime, endTime, usagestats.UsageLogFilters{
|
||||
UserID: userID, APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID,
|
||||
RequestType: requestType, Stream: stream, BillingType: billingType,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, hit, err
|
||||
|
||||
@@ -37,34 +37,36 @@ type dashboardSnapshotV2Response struct {
|
||||
}
|
||||
|
||||
type dashboardSnapshotV2Filters struct {
|
||||
UserID int64
|
||||
APIKeyID int64
|
||||
AccountID int64
|
||||
GroupID int64
|
||||
Model string
|
||||
RequestType *int16
|
||||
Stream *bool
|
||||
BillingType *int8
|
||||
UserID int64
|
||||
APIKeyID int64
|
||||
AccountID int64
|
||||
GroupID int64
|
||||
Model string
|
||||
RequestType *int16
|
||||
Stream *bool
|
||||
BillingType *int8
|
||||
UpstreamModelMismatch *bool
|
||||
}
|
||||
|
||||
type dashboardSnapshotV2CacheKey struct {
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Granularity string `json:"granularity"`
|
||||
UserID int64 `json:"user_id"`
|
||||
APIKeyID int64 `json:"api_key_id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
Model string `json:"model"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
IncludeStats bool `json:"include_stats"`
|
||||
IncludeTrend bool `json:"include_trend"`
|
||||
IncludeModels bool `json:"include_models"`
|
||||
IncludeGroups bool `json:"include_groups"`
|
||||
IncludeUsersTrend bool `json:"include_users_trend"`
|
||||
UsersTrendLimit int `json:"users_trend_limit"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Granularity string `json:"granularity"`
|
||||
UserID int64 `json:"user_id"`
|
||||
APIKeyID int64 `json:"api_key_id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
Model string `json:"model"`
|
||||
RequestType *int16 `json:"request_type"`
|
||||
Stream *bool `json:"stream"`
|
||||
BillingType *int8 `json:"billing_type"`
|
||||
UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
|
||||
IncludeStats bool `json:"include_stats"`
|
||||
IncludeTrend bool `json:"include_trend"`
|
||||
IncludeModels bool `json:"include_models"`
|
||||
IncludeGroups bool `json:"include_groups"`
|
||||
IncludeUsersTrend bool `json:"include_users_trend"`
|
||||
UsersTrendLimit int `json:"users_trend_limit"`
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
|
||||
@@ -93,23 +95,24 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
|
||||
}
|
||||
|
||||
keyRaw, _ := json.Marshal(dashboardSnapshotV2CacheKey{
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
Granularity: granularity,
|
||||
UserID: filters.UserID,
|
||||
APIKeyID: filters.APIKeyID,
|
||||
AccountID: filters.AccountID,
|
||||
GroupID: filters.GroupID,
|
||||
Model: filters.Model,
|
||||
RequestType: filters.RequestType,
|
||||
Stream: filters.Stream,
|
||||
BillingType: filters.BillingType,
|
||||
IncludeStats: includeStats,
|
||||
IncludeTrend: includeTrend,
|
||||
IncludeModels: includeModels,
|
||||
IncludeGroups: includeGroups,
|
||||
IncludeUsersTrend: includeUsersTrend,
|
||||
UsersTrendLimit: usersTrendLimit,
|
||||
StartTime: startTime.UTC().Format(time.RFC3339),
|
||||
EndTime: endTime.UTC().Format(time.RFC3339),
|
||||
Granularity: granularity,
|
||||
UserID: filters.UserID,
|
||||
APIKeyID: filters.APIKeyID,
|
||||
AccountID: filters.AccountID,
|
||||
GroupID: filters.GroupID,
|
||||
Model: filters.Model,
|
||||
RequestType: filters.RequestType,
|
||||
Stream: filters.Stream,
|
||||
BillingType: filters.BillingType,
|
||||
UpstreamModelMismatch: filters.UpstreamModelMismatch,
|
||||
IncludeStats: includeStats,
|
||||
IncludeTrend: includeTrend,
|
||||
IncludeModels: includeModels,
|
||||
IncludeGroups: includeGroups,
|
||||
IncludeUsersTrend: includeUsersTrend,
|
||||
UsersTrendLimit: usersTrendLimit,
|
||||
})
|
||||
cacheKey := string(keyRaw)
|
||||
|
||||
@@ -184,6 +187,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
|
||||
filters.RequestType,
|
||||
filters.Stream,
|
||||
filters.BillingType,
|
||||
filters.UpstreamModelMismatch,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get usage trend")
|
||||
@@ -204,6 +208,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
|
||||
filters.RequestType,
|
||||
filters.Stream,
|
||||
filters.BillingType,
|
||||
filters.UpstreamModelMismatch,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get model statistics")
|
||||
@@ -223,6 +228,7 @@ func (h *DashboardHandler) buildSnapshotV2Response(
|
||||
filters.RequestType,
|
||||
filters.Stream,
|
||||
filters.BillingType,
|
||||
filters.UpstreamModelMismatch,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get group statistics")
|
||||
@@ -299,5 +305,13 @@ func parseDashboardSnapshotV2Filters(c *gin.Context) (*dashboardSnapshotV2Filter
|
||||
filters.BillingType = &bt
|
||||
}
|
||||
|
||||
if mismatchStr := strings.TrimSpace(c.Query("upstream_model_mismatch")); mismatchStr != "" {
|
||||
value, err := strconv.ParseBool(mismatchStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filters.UpstreamModelMismatch = &value
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
const (
|
||||
grokImportProbeConcurrency = 3
|
||||
grokImportProbeTimeout = 25 * time.Second
|
||||
grokImportProbeQueueLimit = 64
|
||||
)
|
||||
|
||||
type grokImportProber interface {
|
||||
@@ -27,6 +28,8 @@ type grokImportProbeTask struct {
|
||||
type grokImportProbeScheduler struct {
|
||||
mu sync.Mutex
|
||||
queue []grokImportProbeTask
|
||||
pending map[int64]struct{}
|
||||
inFlight map[int64]struct{}
|
||||
concurrency int
|
||||
workers int
|
||||
maxWorkers int
|
||||
@@ -48,6 +51,8 @@ func newGrokImportProbeScheduler(concurrency int, timeout time.Duration) *grokIm
|
||||
return &grokImportProbeScheduler{
|
||||
concurrency: concurrency,
|
||||
timeout: timeout,
|
||||
pending: make(map[int64]struct{}),
|
||||
inFlight: make(map[int64]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +65,21 @@ func (s *grokImportProbeScheduler) schedule(prober grokImportProber, account *se
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if _, exists := s.pending[account.ID]; exists {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if _, exists := s.inFlight[account.ID]; exists {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if len(s.queue) >= grokImportProbeQueueLimit {
|
||||
s.mu.Unlock()
|
||||
slog.Debug("grok_import_active_probe_dropped", "account_id", account.ID, "reason", "queue_full")
|
||||
return
|
||||
}
|
||||
s.queue = append(s.queue, grokImportProbeTask{prober: prober, accountID: account.ID})
|
||||
s.pending[account.ID] = struct{}{}
|
||||
if s.workers < s.concurrency {
|
||||
s.workers++
|
||||
if s.workers > s.maxWorkers {
|
||||
@@ -78,6 +97,7 @@ func (s *grokImportProbeScheduler) worker() {
|
||||
return
|
||||
}
|
||||
s.run(task.prober, task.accountID)
|
||||
s.finish(task.accountID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,9 +114,17 @@ func (s *grokImportProbeScheduler) nextTask() (grokImportProbeTask, bool) {
|
||||
if len(s.queue) == 0 {
|
||||
s.queue = nil
|
||||
}
|
||||
delete(s.pending, task.accountID)
|
||||
s.inFlight[task.accountID] = struct{}{}
|
||||
return task, true
|
||||
}
|
||||
|
||||
func (s *grokImportProbeScheduler) finish(accountID int64) {
|
||||
s.mu.Lock()
|
||||
delete(s.inFlight, accountID)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *grokImportProbeScheduler) run(prober grokImportProber, accountID int64) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
@@ -108,8 +136,6 @@ func (s *grokImportProbeScheduler) run(prober grokImportProber, accountID int64)
|
||||
}
|
||||
}()
|
||||
|
||||
// Queue time is intentionally excluded: every imported account is probed,
|
||||
// while this timeout only bounds the actual upstream probe execution.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
|
||||
defer cancel()
|
||||
result, err := prober.QueryQuota(ctx, accountID)
|
||||
|
||||
@@ -4,6 +4,7 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -62,6 +63,10 @@ func (grokImportOAuthClientStub) RefreshToken(context.Context, string, string, s
|
||||
return &xai.TokenResponse{AccessToken: "access-token", RefreshToken: "refresh-token", ExpiresIn: 3600}, nil
|
||||
}
|
||||
|
||||
func (grokImportOAuthClientStub) LoginWithPassword(context.Context, string, string, string) (*service.GrokPasswordLoginResult, error) {
|
||||
return nil, errors.New("unexpected password login")
|
||||
}
|
||||
|
||||
func (grokImportOAuthClientStub) ConvertSSOToBuild(context.Context, string, string) (*xai.TokenResponse, error) {
|
||||
return &xai.TokenResponse{AccessToken: "access-token", RefreshToken: "refresh-token", ExpiresIn: 3600}, nil
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestGrokImportProbeSchedulerProbesSingleAccountOnce(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGrokImportProbeSchedulerQueuesBatchWithoutPerTaskGoroutines(t *testing.T) {
|
||||
const taskCount = 100
|
||||
const taskCount = 50
|
||||
release := make(chan struct{})
|
||||
scheduler := newGrokImportProbeScheduler(3, time.Second)
|
||||
prober := newGrokImportProbeStub(taskCount)
|
||||
@@ -156,7 +156,7 @@ func TestGrokImportProbeSchedulerQueuesBatchWithoutPerTaskGoroutines(t *testing.
|
||||
awaitGrokProbeSignal(t, prober.started)
|
||||
}
|
||||
snapshot := snapshotGrokImportProbeScheduler(scheduler)
|
||||
require.Equal(t, 97, snapshot.queued)
|
||||
require.Equal(t, taskCount-3, snapshot.queued)
|
||||
require.Equal(t, 3, snapshot.workers)
|
||||
require.Equal(t, 3, snapshot.maxWorkers)
|
||||
select {
|
||||
@@ -182,6 +182,55 @@ func TestGrokImportProbeSchedulerQueuesBatchWithoutPerTaskGoroutines(t *testing.
|
||||
require.Equal(t, 3, snapshot.maxWorkers)
|
||||
}
|
||||
|
||||
func TestGrokImportProbeSchedulerDeduplicatesPendingAndInFlightAccounts(t *testing.T) {
|
||||
scheduler := newGrokImportProbeScheduler(1, time.Second)
|
||||
prober := newGrokImportProbeStub(2)
|
||||
release := make(chan struct{})
|
||||
prober.block = release
|
||||
account := newGrokOAuthImportAccount(501)
|
||||
queued := newGrokOAuthImportAccount(502)
|
||||
|
||||
scheduler.schedule(prober, account)
|
||||
require.Equal(t, int64(501), awaitGrokProbeSignal(t, prober.started))
|
||||
scheduler.schedule(prober, account)
|
||||
scheduler.schedule(prober, queued)
|
||||
scheduler.schedule(prober, queued)
|
||||
|
||||
scheduler.mu.Lock()
|
||||
require.Len(t, scheduler.queue, 1)
|
||||
require.Contains(t, scheduler.inFlight, int64(501))
|
||||
require.Contains(t, scheduler.pending, int64(502))
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
close(release)
|
||||
require.Equal(t, int64(501), awaitGrokProbeSignal(t, prober.done))
|
||||
require.Equal(t, int64(502), awaitGrokProbeSignal(t, prober.done))
|
||||
calls, _, _ := prober.snapshot()
|
||||
require.Equal(t, 1, calls[501])
|
||||
require.Equal(t, 1, calls[502])
|
||||
}
|
||||
|
||||
func TestGrokImportProbeSchedulerBoundsPendingQueue(t *testing.T) {
|
||||
scheduler := newGrokImportProbeScheduler(1, time.Second)
|
||||
prober := newGrokImportProbeStub(grokImportProbeQueueLimit + 1)
|
||||
release := make(chan struct{})
|
||||
prober.block = release
|
||||
scheduler.schedule(prober, newGrokOAuthImportAccount(600))
|
||||
require.Equal(t, int64(600), awaitGrokProbeSignal(t, prober.started))
|
||||
for id := int64(601); id < 601+grokImportProbeQueueLimit+10; id++ {
|
||||
scheduler.schedule(prober, newGrokOAuthImportAccount(id))
|
||||
}
|
||||
|
||||
scheduler.mu.Lock()
|
||||
require.Len(t, scheduler.queue, grokImportProbeQueueLimit)
|
||||
scheduler.mu.Unlock()
|
||||
|
||||
close(release)
|
||||
for i := 0; i < grokImportProbeQueueLimit+1; i++ {
|
||||
awaitGrokProbeSignal(t, prober.done)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrokImportProbeSchedulerTimeoutCancelsProbe(t *testing.T) {
|
||||
neverRelease := make(chan struct{})
|
||||
scheduler := newGrokImportProbeScheduler(1, 20*time.Millisecond)
|
||||
|
||||
@@ -47,6 +47,10 @@ type GrokGenerateAuthURLRequest struct {
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
func (h *GrokOAuthHandler) GetCapabilities(c *gin.Context) {
|
||||
response.Success(c, h.grokOAuthService.GetCapabilities())
|
||||
}
|
||||
|
||||
func (h *GrokOAuthHandler) GenerateAuthURL(c *gin.Context) {
|
||||
var req GrokGenerateAuthURLRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -95,6 +99,17 @@ type GrokRefreshTokenRequest struct {
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
}
|
||||
|
||||
type GrokSSOTokenRequest struct {
|
||||
SSOToken string `json:"sso_token"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
}
|
||||
|
||||
type GrokPasswordAuthorizeRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
}
|
||||
|
||||
func (h *GrokOAuthHandler) RefreshToken(c *gin.Context) {
|
||||
var req GrokRefreshTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -113,9 +128,15 @@ func (h *GrokOAuthHandler) RefreshToken(c *gin.Context) {
|
||||
var proxyURL string
|
||||
if req.ProxyID != nil {
|
||||
proxy, err := h.adminService.GetProxy(c.Request.Context(), *req.ProxyID)
|
||||
if err == nil && proxy != nil {
|
||||
proxyURL = proxy.URL()
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if proxy == nil {
|
||||
response.BadRequest(c, "GROK_OAUTH_PROXY_NOT_FOUND: proxy not found")
|
||||
return
|
||||
}
|
||||
proxyURL = proxy.URL()
|
||||
}
|
||||
tokenInfo, err := h.grokOAuthService.RefreshToken(c.Request.Context(), refreshToken, proxyURL, req.ClientID)
|
||||
if err != nil {
|
||||
@@ -125,6 +146,38 @@ func (h *GrokOAuthHandler) RefreshToken(c *gin.Context) {
|
||||
response.Success(c, tokenInfo)
|
||||
}
|
||||
|
||||
// ValidateSSOToken converts a Web SSO cookie into Build OAuth tokens.
|
||||
// Response contains OAuth token info only — never echoes sso_token.
|
||||
func (h *GrokOAuthHandler) ValidateSSOToken(c *gin.Context) {
|
||||
var req GrokSSOTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
tokenInfo, err := h.grokOAuthService.ValidateSSOToken(c.Request.Context(), req.SSOToken, req.ProxyID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, tokenInfo)
|
||||
}
|
||||
|
||||
// AuthorizePassword exchanges email/password for Build OAuth tokens via SSO conversion.
|
||||
// Response never includes password or raw sso_token.
|
||||
func (h *GrokOAuthHandler) AuthorizePassword(c *gin.Context) {
|
||||
var req GrokPasswordAuthorizeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
tokenInfo, err := h.grokOAuthService.AuthorizePassword(c.Request.Context(), req.Email, req.Password, req.ProxyID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, tokenInfo)
|
||||
}
|
||||
|
||||
func (h *GrokOAuthHandler) RefreshAccountToken(c *gin.Context) {
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -412,11 +465,38 @@ func (h *GrokOAuthHandler) createAccountFromSSOToken(ctx context.Context, req Gr
|
||||
// 配置且 Build 恒写官方地址,会吞掉导入时指定的自定义转发地址——与
|
||||
// RefreshAccountToken 的保留逻辑对齐,请求显式提供时以请求为准。
|
||||
func grokSSOImportCredentials(built map[string]any, reqCredentials map[string]any) map[string]any {
|
||||
credentials := service.MergeCredentials(cloneGrokSSOMap(reqCredentials), built)
|
||||
// Only merge operator config from the request — never free-form secrets
|
||||
// (password / sso_token / cookie / etc.) into stored credentials.
|
||||
allowedReqKeys := map[string]struct{}{
|
||||
"base_url": {}, "model_mapping": {},
|
||||
"header_override": {}, "header_overrides": {}, "header_override_enabled": {},
|
||||
"custom_headers": {},
|
||||
}
|
||||
ops := map[string]any{}
|
||||
for k, v := range reqCredentials {
|
||||
if _, ok := allowedReqKeys[k]; !ok {
|
||||
continue
|
||||
}
|
||||
if service.IsSensitiveCredentialKey(k) {
|
||||
continue
|
||||
}
|
||||
ops[k] = v
|
||||
}
|
||||
credentials := service.MergeCredentials(ops, built)
|
||||
// Strip any sensitive keys that might have slipped in via older callers.
|
||||
for k := range credentials {
|
||||
if service.IsSensitiveCredentialKey(k) {
|
||||
// Keep only keys produced by BuildAccountCredentials (tokens).
|
||||
if k == "access_token" || k == "refresh_token" || k == "id_token" {
|
||||
continue
|
||||
}
|
||||
delete(credentials, k)
|
||||
}
|
||||
}
|
||||
if reqBaseURL, ok := reqCredentials["base_url"].(string); ok && strings.TrimSpace(reqBaseURL) != "" {
|
||||
credentials["base_url"] = strings.TrimSpace(reqBaseURL)
|
||||
}
|
||||
return credentials
|
||||
return service.SanitizeStoredCredentials(service.PlatformGrok, credentials)
|
||||
}
|
||||
|
||||
func grokSSOImportExpiry(requestExpiresAt *int64, requestAutoPause *bool, tokenInfo *service.GrokTokenInfo) (*int64, *bool) {
|
||||
@@ -535,8 +615,10 @@ func (h *GrokOAuthHandler) ResetQuota(c *gin.Context) {
|
||||
response.BadRequest(c, "grok quota service is not enabled")
|
||||
return
|
||||
}
|
||||
// ResetQuota 恒返回 GROK_QUOTA_RESET_UNSUPPORTED(xAI 无 OAuth 配额重置接口),err != nil 恒真为预期。
|
||||
//nolint:staticcheck // SA4023
|
||||
result, err := h.quotaService.ResetQuota(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
if err != nil { //nolint:staticcheck // SA4023
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -128,14 +130,22 @@ func TestGrokOAuthHandlerQueryQuotaProbesUpstream(t *testing.T) {
|
||||
require.Contains(t, rec.Body.String(), `"snapshot":`)
|
||||
require.Contains(t, rec.Body.String(), `"headers_observed":true`)
|
||||
require.NotContains(t, rec.Body.String(), "access-token")
|
||||
require.Eventually(t, func() bool {
|
||||
upstream.mu.Lock()
|
||||
defer upstream.mu.Unlock()
|
||||
return len(upstream.requests) == 4
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
upstream.mu.Lock()
|
||||
requests := append([]*http.Request(nil), upstream.requests...)
|
||||
bodies := append([][]byte(nil), upstream.bodies...)
|
||||
upstream.mu.Unlock()
|
||||
require.Len(t, requests, 3)
|
||||
require.Len(t, requests, 4)
|
||||
responsesProbeSeen := false
|
||||
modelsSyncSeen := false
|
||||
for i, upstreamReq := range requests {
|
||||
require.Equal(t, "Bearer access-token", upstreamReq.Header.Get("Authorization"))
|
||||
if upstreamReq.URL.String() == xai.DefaultCLIBaseURL+"/responses" {
|
||||
responsesProbeSeen = true
|
||||
require.Equal(t, "application/json, text/event-stream", upstreamReq.Header.Get("Accept"))
|
||||
require.Contains(t, string(bodies[i]), `"model":"grok-4.5"`)
|
||||
require.Contains(t, string(bodies[i]), `"input":"hi"`)
|
||||
@@ -143,7 +153,12 @@ func TestGrokOAuthHandlerQueryQuotaProbesUpstream(t *testing.T) {
|
||||
require.NotContains(t, string(bodies[i]), `"max_output_tokens"`)
|
||||
require.NotContains(t, string(bodies[i]), `"store"`)
|
||||
}
|
||||
if upstreamReq.URL.String() == xai.DefaultCLIBaseURL+"/models" {
|
||||
modelsSyncSeen = true
|
||||
}
|
||||
}
|
||||
require.True(t, responsesProbeSeen)
|
||||
require.True(t, modelsSyncSeen)
|
||||
require.NotNil(t, repo.updates[42])
|
||||
}
|
||||
|
||||
@@ -189,6 +204,84 @@ func TestGrokOAuthHandlerRuntimeSanityDoesNotExposeSecrets(t *testing.T) {
|
||||
require.NotContains(t, rec.Body.String(), "client-secret-like-value")
|
||||
}
|
||||
|
||||
type grokOAuthHandlerClient struct{}
|
||||
|
||||
func (c *grokOAuthHandlerClient) ExchangeCode(context.Context, string, string, string, string, string) (*xai.TokenResponse, error) {
|
||||
return nil, errors.New("unexpected exchange")
|
||||
}
|
||||
|
||||
func (c *grokOAuthHandlerClient) RefreshToken(context.Context, string, string, string) (*xai.TokenResponse, error) {
|
||||
return &xai.TokenResponse{AccessToken: "access-token", RefreshToken: "refresh-token", ExpiresIn: 3600}, nil
|
||||
}
|
||||
|
||||
func (c *grokOAuthHandlerClient) LoginWithPassword(_ context.Context, email, _ string, _ string) (*service.GrokPasswordLoginResult, error) {
|
||||
return &service.GrokPasswordLoginResult{
|
||||
Email: email,
|
||||
SSOToken: "sso-from-password",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *grokOAuthHandlerClient) ConvertSSOToBuild(context.Context, string, string) (*xai.TokenResponse, error) {
|
||||
return &xai.TokenResponse{AccessToken: "access-token", RefreshToken: "refresh-token", ExpiresIn: 3600}, nil
|
||||
}
|
||||
|
||||
func TestGrokOAuthHandlerValidateSSOTokenReturnsTokenInfo(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
oauthClient := &grokOAuthHandlerClient{}
|
||||
oauthService := service.NewGrokOAuthService(nil, oauthClient)
|
||||
defer oauthService.Stop()
|
||||
handler := NewGrokOAuthHandler(oauthService, nil, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/grok/oauth/sso-token", handler.ValidateSSOToken)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/grok/oauth/sso-token", strings.NewReader(`{"sso_token":"sso-token"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), `"access_token":"access-token"`)
|
||||
require.NotContains(t, rec.Body.String(), `"sso_token"`)
|
||||
}
|
||||
|
||||
func TestGrokOAuthHandlerAuthorizePasswordReturnsTokenInfoWithoutPassword(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
oauthClient := &grokOAuthHandlerClient{}
|
||||
cfg := &config.Config{}
|
||||
cfg.Gateway.Grok.PasswordAuthEnabled = true
|
||||
oauthService := service.NewGrokOAuthService(nil, oauthClient, cfg)
|
||||
defer oauthService.Stop()
|
||||
handler := NewGrokOAuthHandler(oauthService, nil, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/grok/oauth/password", handler.AuthorizePassword)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/grok/oauth/password", strings.NewReader(`{"email":"user@example.com","password":"super-secret"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), `"access_token":"access-token"`)
|
||||
require.NotContains(t, rec.Body.String(), "super-secret")
|
||||
}
|
||||
|
||||
func TestGrokOAuthHandlerPasswordCapabilityDefaultsToDisabled(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
oauthService := service.NewGrokOAuthService(nil, &grokOAuthHandlerClient{})
|
||||
defer oauthService.Stop()
|
||||
handler := NewGrokOAuthHandler(oauthService, nil, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/admin/grok/oauth/capabilities", handler.GetCapabilities)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/admin/grok/oauth/capabilities", nil))
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), `"password_auth_enabled":false`)
|
||||
}
|
||||
|
||||
func TestGrokSSOImportExpiryUsesTokenExpiryWithoutRefreshToken(t *testing.T) {
|
||||
tokenExpiry := time.Now().Add(6 * time.Hour).Unix()
|
||||
expiresAt, autoPause := grokSSOImportExpiry(nil, nil, &service.GrokTokenInfo{
|
||||
@@ -266,14 +359,12 @@ func TestGrokSSOImportCredentialsDefaultsToOfficialBaseURL(t *testing.T) {
|
||||
require.Equal(t, "at-2", credentials["access_token"])
|
||||
}
|
||||
|
||||
func TestGrokSSOImportWorkerRecoversPanic(t *testing.T) {
|
||||
func TestGrokSSOImportWorkerHandlesMissingOAuthService(t *testing.T) {
|
||||
h := &GrokOAuthHandler{}
|
||||
result := h.safeCreateAccountFromSSOToken(context.Background(), GrokSSOToOAuthRequest{}, "token", 2, 3)
|
||||
// Without a service, createAccountFromSSOToken would panic on nil service access.
|
||||
// Recovery must convert that into a failed item and keep the worker alive.
|
||||
require.False(t, result.created)
|
||||
require.Equal(t, 2, result.item.Index)
|
||||
require.Contains(t, result.item.Error, "internal worker panic")
|
||||
require.Contains(t, result.item.Error, "GROK_OAUTH_CLIENT_NOT_CONFIGURED")
|
||||
}
|
||||
|
||||
func TestGrokOAuthHandlerReconcileDefaultsToDryRun(t *testing.T) {
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||
"github.com/Wei-Shaw/sub2api/internal/platform/liveattestation"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
@@ -96,38 +96,48 @@ func NewGroupHandler(adminService service.AdminService, dashboardService *servic
|
||||
|
||||
// CreateGroupRequest represents create group request
|
||||
type CreateGroupRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok composite"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
IsExclusive bool `json:"is_exclusive"`
|
||||
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
|
||||
DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
|
||||
WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
|
||||
MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
IsExclusive bool `json:"is_exclusive"`
|
||||
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
|
||||
DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
|
||||
WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
|
||||
MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
|
||||
LongContextPricingEnabled bool `json:"long_context_pricing_enabled"`
|
||||
ModelPricing []service.ChannelModelPricing `json:"model_pricing"`
|
||||
// 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
|
||||
AllowImageGeneration bool `json:"allow_image_generation"`
|
||||
AllowBatchImageGeneration bool `json:"allow_batch_image_generation"`
|
||||
ImageRateIndependent bool `json:"image_rate_independent"`
|
||||
ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
|
||||
BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"`
|
||||
BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"`
|
||||
VideoRateIndependent bool `json:"video_rate_independent"`
|
||||
VideoRateMultiplier *float64 `json:"video_rate_multiplier"`
|
||||
PeakRateEnabled bool `json:"peak_rate_enabled"`
|
||||
PeakStart string `json:"peak_start"`
|
||||
PeakEnd string `json:"peak_end"`
|
||||
PeakRateMultiplier *float64 `json:"peak_rate_multiplier"`
|
||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||
VideoPrice480P *float64 `json:"video_price_480p"`
|
||||
VideoPrice720P *float64 `json:"video_price_720p"`
|
||||
VideoPrice1080P *float64 `json:"video_price_1080p"`
|
||||
WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
|
||||
ClaudeCodeOnly bool `json:"claude_code_only"`
|
||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
||||
AllowImageGeneration bool `json:"allow_image_generation"`
|
||||
AllowBatchImageGeneration bool `json:"allow_batch_image_generation"`
|
||||
ImageRateIndependent bool `json:"image_rate_independent"`
|
||||
ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
|
||||
BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"`
|
||||
BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"`
|
||||
VideoRateIndependent bool `json:"video_rate_independent"`
|
||||
VideoRateMultiplier *float64 `json:"video_rate_multiplier"`
|
||||
PeakRateEnabled bool `json:"peak_rate_enabled"`
|
||||
PeakStart string `json:"peak_start"`
|
||||
PeakEnd string `json:"peak_end"`
|
||||
PeakRateMultiplier *float64 `json:"peak_rate_multiplier"`
|
||||
ProfitControlEnabled bool `json:"profit_control_enabled"`
|
||||
ProfitMinMargin *float64 `json:"profit_min_margin"`
|
||||
ProfitSafetyBuffer *float64 `json:"profit_safety_buffer"`
|
||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||
VideoPrice480P *float64 `json:"video_price_480p"`
|
||||
VideoPrice720P *float64 `json:"video_price_720p"`
|
||||
VideoPrice1080P *float64 `json:"video_price_1080p"`
|
||||
VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"`
|
||||
WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
|
||||
SearchPricePer1k *float64 `json:"search_price_per_1k"`
|
||||
AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"`
|
||||
AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"`
|
||||
AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour"`
|
||||
ClaudeCodeOnly bool `json:"claude_code_only"`
|
||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
||||
// 模型路由配置(仅 anthropic 平台使用)
|
||||
ModelRouting map[string][]int64 `json:"model_routing"`
|
||||
ModelRoutingEnabled bool `json:"model_routing_enabled"`
|
||||
@@ -154,39 +164,49 @@ type CreateGroupRequest struct {
|
||||
|
||||
// UpdateGroupRequest represents update group request
|
||||
type UpdateGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok composite"`
|
||||
RateMultiplier *float64 `json:"rate_multiplier"`
|
||||
IsExclusive *bool `json:"is_exclusive"`
|
||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
|
||||
DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
|
||||
WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
|
||||
MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"`
|
||||
RateMultiplier *float64 `json:"rate_multiplier"`
|
||||
IsExclusive *bool `json:"is_exclusive"`
|
||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
|
||||
DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
|
||||
WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
|
||||
MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
|
||||
LongContextPricingEnabled *bool `json:"long_context_pricing_enabled"`
|
||||
ModelPricing *[]service.ChannelModelPricing `json:"model_pricing"`
|
||||
// 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
|
||||
AllowImageGeneration *bool `json:"allow_image_generation"`
|
||||
AllowBatchImageGeneration *bool `json:"allow_batch_image_generation"`
|
||||
ImageRateIndependent *bool `json:"image_rate_independent"`
|
||||
ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
|
||||
BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"`
|
||||
BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"`
|
||||
VideoRateIndependent *bool `json:"video_rate_independent"`
|
||||
VideoRateMultiplier *float64 `json:"video_rate_multiplier"`
|
||||
PeakRateEnabled *bool `json:"peak_rate_enabled"`
|
||||
PeakStart *string `json:"peak_start"`
|
||||
PeakEnd *string `json:"peak_end"`
|
||||
PeakRateMultiplier *float64 `json:"peak_rate_multiplier"`
|
||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||
VideoPrice480P *float64 `json:"video_price_480p"`
|
||||
VideoPrice720P *float64 `json:"video_price_720p"`
|
||||
VideoPrice1080P *float64 `json:"video_price_1080p"`
|
||||
WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
|
||||
ClaudeCodeOnly *bool `json:"claude_code_only"`
|
||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
||||
AllowImageGeneration *bool `json:"allow_image_generation"`
|
||||
AllowBatchImageGeneration *bool `json:"allow_batch_image_generation"`
|
||||
ImageRateIndependent *bool `json:"image_rate_independent"`
|
||||
ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
|
||||
BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"`
|
||||
BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"`
|
||||
VideoRateIndependent *bool `json:"video_rate_independent"`
|
||||
VideoRateMultiplier *float64 `json:"video_rate_multiplier"`
|
||||
PeakRateEnabled *bool `json:"peak_rate_enabled"`
|
||||
PeakStart *string `json:"peak_start"`
|
||||
PeakEnd *string `json:"peak_end"`
|
||||
PeakRateMultiplier *float64 `json:"peak_rate_multiplier"`
|
||||
ProfitControlEnabled *bool `json:"profit_control_enabled"`
|
||||
ProfitMinMargin *float64 `json:"profit_min_margin"`
|
||||
ProfitSafetyBuffer *float64 `json:"profit_safety_buffer"`
|
||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||
VideoPrice480P *float64 `json:"video_price_480p"`
|
||||
VideoPrice720P *float64 `json:"video_price_720p"`
|
||||
VideoPrice1080P *float64 `json:"video_price_1080p"`
|
||||
VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"`
|
||||
WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
|
||||
SearchPricePer1k *float64 `json:"search_price_per_1k"`
|
||||
AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"`
|
||||
AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"`
|
||||
AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour"`
|
||||
ClaudeCodeOnly *bool `json:"claude_code_only"`
|
||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
||||
// 模型路由配置(仅 anthropic 平台使用)
|
||||
ModelRouting map[string][]int64 `json:"model_routing"`
|
||||
ModelRoutingEnabled *bool `json:"model_routing_enabled"`
|
||||
@@ -214,7 +234,7 @@ type UpdateGroupRequest struct {
|
||||
type CompositeRouteRequest struct {
|
||||
PublicModel string `json:"public_model" binding:"required"`
|
||||
MatchType string `json:"match_type" binding:"omitempty,oneof=exact prefix"`
|
||||
TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok"`
|
||||
TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek"`
|
||||
UpstreamModel string `json:"upstream_model"`
|
||||
Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
|
||||
Priority int `json:"priority"`
|
||||
@@ -475,6 +495,13 @@ func (h *GroupHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// platform 是 omitempty:预校验必须用与 CreateGroup 落库一致的归一化平台,
|
||||
// 否则省略 platform 的请求会被误判成「平台不支持利润控制」。
|
||||
if err := service.ValidateProfitControlConfig(service.NormalizeGroupPlatform(req.Platform), req.ProfitControlEnabled, float64ValueOrDefault(req.ProfitMinMargin, 0), float64ValueOrDefault(req.ProfitSafetyBuffer, 0)); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.adminService.CreateGroup(c.Request.Context(), &service.CreateGroupInput{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
@@ -485,6 +512,8 @@ func (h *GroupHandler) Create(c *gin.Context) {
|
||||
DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(),
|
||||
WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(),
|
||||
MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(),
|
||||
LongContextPricingEnabled: req.LongContextPricingEnabled,
|
||||
ModelPricing: req.ModelPricing,
|
||||
AllowImageGeneration: req.AllowImageGeneration,
|
||||
AllowBatchImageGeneration: req.AllowBatchImageGeneration,
|
||||
ImageRateIndependent: req.ImageRateIndependent,
|
||||
@@ -497,13 +526,21 @@ func (h *GroupHandler) Create(c *gin.Context) {
|
||||
PeakStart: req.PeakStart,
|
||||
PeakEnd: req.PeakEnd,
|
||||
PeakRateMultiplier: req.PeakRateMultiplier,
|
||||
ProfitControlEnabled: req.ProfitControlEnabled,
|
||||
ProfitMinMargin: req.ProfitMinMargin,
|
||||
ProfitSafetyBuffer: req.ProfitSafetyBuffer,
|
||||
ImagePrice1K: req.ImagePrice1K,
|
||||
ImagePrice2K: req.ImagePrice2K,
|
||||
ImagePrice4K: req.ImagePrice4K,
|
||||
VideoPrice480P: req.VideoPrice480P,
|
||||
VideoPrice720P: req.VideoPrice720P,
|
||||
VideoPrice1080P: req.VideoPrice1080P,
|
||||
VideoModelPrices: req.VideoModelPrices,
|
||||
WebSearchPricePerCall: req.WebSearchPricePerCall,
|
||||
SearchPricePer1k: req.SearchPricePer1k,
|
||||
AudioRealtimePricePerMin: req.AudioRealtimePricePerMin,
|
||||
AudioTTSPricePerMillionChars: req.AudioTtsPricePerMillionChars,
|
||||
AudioSTTPricePerHour: req.AudioSttPricePerHour,
|
||||
ClaudeCodeOnly: req.ClaudeCodeOnly,
|
||||
FallbackGroupID: req.FallbackGroupID,
|
||||
FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
|
||||
@@ -604,6 +641,8 @@ func (h *GroupHandler) Update(c *gin.Context) {
|
||||
DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(),
|
||||
WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(),
|
||||
MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(),
|
||||
LongContextPricingEnabled: req.LongContextPricingEnabled,
|
||||
ModelPricing: req.ModelPricing,
|
||||
AllowImageGeneration: req.AllowImageGeneration,
|
||||
AllowBatchImageGeneration: req.AllowBatchImageGeneration,
|
||||
ImageRateIndependent: req.ImageRateIndependent,
|
||||
@@ -616,13 +655,21 @@ func (h *GroupHandler) Update(c *gin.Context) {
|
||||
PeakStart: req.PeakStart,
|
||||
PeakEnd: req.PeakEnd,
|
||||
PeakRateMultiplier: req.PeakRateMultiplier,
|
||||
ProfitControlEnabled: req.ProfitControlEnabled,
|
||||
ProfitMinMargin: req.ProfitMinMargin,
|
||||
ProfitSafetyBuffer: req.ProfitSafetyBuffer,
|
||||
ImagePrice1K: req.ImagePrice1K,
|
||||
ImagePrice2K: req.ImagePrice2K,
|
||||
ImagePrice4K: req.ImagePrice4K,
|
||||
VideoPrice480P: req.VideoPrice480P,
|
||||
VideoPrice720P: req.VideoPrice720P,
|
||||
VideoPrice1080P: req.VideoPrice1080P,
|
||||
VideoModelPrices: req.VideoModelPrices,
|
||||
WebSearchPricePerCall: req.WebSearchPricePerCall,
|
||||
SearchPricePer1k: req.SearchPricePer1k,
|
||||
AudioRealtimePricePerMin: req.AudioRealtimePricePerMin,
|
||||
AudioTTSPricePerMillionChars: req.AudioTtsPricePerMillionChars,
|
||||
AudioSTTPricePerHour: req.AudioSttPricePerHour,
|
||||
ClaudeCodeOnly: req.ClaudeCodeOnly,
|
||||
FallbackGroupID: req.FallbackGroupID,
|
||||
FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
|
||||
@@ -687,12 +734,10 @@ func (h *GroupHandler) GetStats(c *gin.Context) {
|
||||
_ = groupID // TODO: implement actual stats
|
||||
}
|
||||
|
||||
// GetUsageSummary returns today's and cumulative cost for all groups.
|
||||
// GET /api/v1/admin/groups/usage-summary?timezone=Asia/Shanghai
|
||||
// GetUsageSummary returns today's, yesterday's, and cumulative cost for all groups.
|
||||
// GET /api/v1/admin/groups/usage-summary
|
||||
func (h *GroupHandler) GetUsageSummary(c *gin.Context) {
|
||||
userTZ := c.Query("timezone")
|
||||
now := timezone.NowInUserLocation(userTZ)
|
||||
todayStart := timezone.StartOfDayInUserLocation(now, userTZ)
|
||||
todayStart := service.GroupUsageTodayStart(time.Now())
|
||||
|
||||
results, err := h.dashboardService.GetGroupUsageSummary(c.Request.Context(), todayStart)
|
||||
if err != nil {
|
||||
|
||||