Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155c494964 | ||
|
|
8991574873 | ||
|
|
8f7b0a314d | ||
|
|
64090de664 | ||
|
|
de904c433e | ||
|
|
14a27f1960 | ||
|
|
c33c3208e3 | ||
|
|
db0bff82c7 | ||
|
|
e88fc52ce6 | ||
|
|
045b620d0d | ||
|
|
e79429b03f | ||
|
|
02fbcbe3ad | ||
|
|
b5d9fd21b0 | ||
|
|
146b8b6684 | ||
|
|
a071b27b41 | ||
|
|
379db19141 | ||
|
|
f3c94d2099 | ||
|
|
b8c0c00091 | ||
|
|
bd9b12bbcb | ||
|
|
e687ca3e9d | ||
|
|
99b357083e | ||
|
|
32e4de7942 | ||
|
|
8f55e0a7cd | ||
|
|
5ec11885d1 | ||
|
|
c8af48d769 | ||
|
|
dce0f6b4e3 | ||
|
|
22ef761c27 | ||
|
|
865002fd2c | ||
|
|
93367b6db4 | ||
|
|
b6e53c9320 | ||
|
|
dbb42881c0 | ||
|
|
e4b0e1b66b | ||
|
|
02e50cc22d | ||
|
|
287a9f386b | ||
|
|
8e102b3a0f | ||
|
|
e93f6b9953 | ||
|
|
a19c9f8d8a | ||
|
|
a1936d42db | ||
|
|
c123caddd4 | ||
|
|
e08aee49ed | ||
|
|
c9e60d1f26 | ||
|
|
47c03c75d8 | ||
|
|
5b7a64334f | ||
|
|
82ccc187cd | ||
|
|
ce14983133 | ||
|
|
fc5a1b78d2 | ||
|
|
00b8596176 | ||
|
|
c5e046b7d7 | ||
|
|
aac53afe0e | ||
|
|
f0e7a9c7a2 | ||
|
|
26e0a89323 | ||
|
|
d431c57f2e | ||
|
|
635a27189f | ||
|
|
d3b5703b55 | ||
|
|
7d38e67120 | ||
|
|
8b3fe664dc | ||
|
|
a4d263f62f | ||
|
|
ae81dfd933 | ||
|
|
35cab3c814 | ||
|
|
770e35b474 | ||
|
|
846dd310a3 | ||
|
|
9b4575e434 | ||
|
|
1f4cfc44c1 | ||
|
|
e592c5f9e0 | ||
|
|
9fd7e76231 | ||
|
|
785b61d424 | ||
|
|
2d3e845205 | ||
|
|
80af44cad7 | ||
|
|
c899c8cf37 | ||
|
|
4c4ff36380 | ||
|
|
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 | ||
|
|
017f6bbd5e | ||
|
|
d6467f6eb0 | ||
|
|
8ed9f754cf | ||
|
|
3deb2f17d8 | ||
|
|
f9d2791693 | ||
|
|
105e5c5da3 | ||
|
|
baaae8e121 | ||
|
|
bf0fc03ab7 | ||
|
|
2be08f3f39 | ||
|
|
493955f7bd | ||
|
|
53aa5cd247 | ||
|
|
0a45be17d8 | ||
|
|
60f6dc91cf | ||
|
|
1702ee1362 | ||
|
|
c4b461c68c | ||
|
|
3c387a164d | ||
|
|
e854132a57 | ||
|
|
a8cd33eead | ||
|
|
276d9cbd9b | ||
|
|
1f8b3a9c60 | ||
|
|
c9156c1e7f | ||
|
|
91850d36e6 | ||
|
|
5c9629ddb7 | ||
|
|
4d13925c9e | ||
|
|
6fa784fdd0 | ||
|
|
da49ce3f29 | ||
|
|
313121f3f7 | ||
|
|
488d3b09ec | ||
|
|
d74e669a23 | ||
|
|
b2d895fb8d | ||
|
|
7d3bf86e55 | ||
|
|
2a871ec852 | ||
|
|
a35ff9613e | ||
|
|
739c0ff9c5 | ||
|
|
3d99acb0ad | ||
|
|
fb40211305 | ||
|
|
7b6111f2f7 | ||
|
|
352b21f4e2 | ||
|
|
0ee9ea5765 | ||
|
|
711056f5b7 | ||
|
|
ac90355a87 | ||
|
|
54b1f8f6b3 | ||
|
|
b74cb7891c | ||
|
|
0010894f99 | ||
|
|
0353cdadfe | ||
|
|
92dc61d401 | ||
|
|
5a6143097d | ||
|
|
b9c7cb8e24 | ||
|
|
99c8e4bf75 | ||
|
|
f2d824836f | ||
|
|
6e1cbed423 | ||
|
|
e3e033bb35 | ||
|
|
acad7f1a09 | ||
|
|
8fd01c2814 | ||
|
|
39903f006e | ||
|
|
32618e71e4 | ||
|
|
86fb4781f4 | ||
|
|
720c405e35 | ||
|
|
2e432173f7 | ||
|
|
38ef8dc069 | ||
|
|
edac19e87a | ||
|
|
97f44b21bb | ||
|
|
0ef2228ce9 | ||
|
|
1cf6972e73 | ||
|
|
20893dc4fd | ||
|
|
a12d88ed4b | ||
|
|
6c04e41952 | ||
|
|
f71332ff85 | ||
|
|
c342a885b5 | ||
|
|
1b966dbea1 | ||
|
|
7e65eafbe4 | ||
|
|
1aeacf4d41 | ||
|
|
d95f6b98ce | ||
|
|
bfbe113f5e | ||
|
|
d8ae153ae9 | ||
|
|
272735b0a7 | ||
|
|
248236ce6d | ||
|
|
1c26dc7ad8 | ||
|
|
1631b19f84 | ||
|
|
3c62b5ca85 | ||
|
|
fc495e087d | ||
|
|
a6fc2d10b3 | ||
|
|
59ce11c780 | ||
|
|
dc893dd0b8 | ||
|
|
f18f3143e2 | ||
|
|
fead4c7ec3 | ||
|
|
46dba19397 | ||
|
|
83b368553d | ||
|
|
d96b6a31ff | ||
|
|
ab73bc0c77 | ||
|
|
3ce8efc125 | ||
|
|
cc84cd8b4c | ||
|
|
95590b5530 | ||
|
|
b765a7f9f6 | ||
|
|
b72d487b85 | ||
|
|
ad34f89152 | ||
|
|
de6b189a6b | ||
|
|
a74e11c26a | ||
|
|
131d42d25d | ||
|
|
031c83b7e0 | ||
|
|
7a3fda57c8 | ||
|
|
16365199aa | ||
|
|
91a2281c7a | ||
|
|
ece9517091 | ||
|
|
4cc88e27b6 | ||
|
|
b468e428e9 | ||
|
|
a40d6de12e | ||
|
|
bc9173be15 | ||
|
|
eb6e3d1f1d | ||
|
|
a93bfb6623 | ||
|
|
8f47bd5fa0 | ||
|
|
beeb4b84ed | ||
|
|
aac44473aa | ||
|
|
465362e1af | ||
|
|
6ee2304dcd | ||
|
|
e94383a4c4 | ||
|
|
7d3a896fcd | ||
|
|
7dde9370e4 | ||
|
|
5c471485ab | ||
|
|
7b3ed2a961 | ||
|
|
be65c713ff | ||
|
|
78f78947f1 | ||
|
|
005a5d2a37 | ||
|
|
1f45c99de7 | ||
|
|
357c5b917b | ||
|
|
4158e73b3f | ||
|
|
cc62979aa7 | ||
|
|
efa5a2240d | ||
|
|
3e08106116 | ||
|
|
2db0cbd292 | ||
|
|
71d7f86883 | ||
|
|
7af28ca843 | ||
|
|
16dd3d8ee6 | ||
|
|
56b5f0df68 | ||
|
|
7ce6e8d652 | ||
|
|
a36222748d | ||
|
|
2447c44f85 | ||
|
|
fd7e2039d3 | ||
|
|
ba5fa6a38f | ||
|
|
6d99e668d2 | ||
|
|
0875143d98 | ||
|
|
d11b838702 | ||
|
|
6c7625800a | ||
|
|
1850e00955 | ||
|
|
291a737422 | ||
|
|
1614ae9c99 | ||
|
|
2730c1c43b | ||
|
|
0b5903d458 | ||
|
|
e9a58c1cb8 | ||
|
|
ef0ca5bdf5 | ||
|
|
bc3acd6e28 | ||
|
|
b6f9277515 | ||
|
|
2e2638c01d | ||
|
|
1763db3a2d | ||
|
|
bb0c38306f | ||
|
|
b403f88f51 | ||
|
|
f1eaed31ed | ||
|
|
0f72b7dca7 | ||
|
|
43d4bae246 | ||
|
|
5374ce2a0f | ||
|
|
7acc13a29b | ||
|
|
b99ea955d9 | ||
|
|
e6a3030beb | ||
|
|
3176cdee17 | ||
|
|
6b267e4f40 | ||
|
|
f06c8c5698 | ||
|
|
8bed40c67b | ||
|
|
a29a62112b | ||
|
|
eb03be0dbf | ||
|
|
6d956bdc20 | ||
|
|
333acde7d1 | ||
|
|
9994eaa70f | ||
|
|
a9866f03db | ||
|
|
0e39e21fa2 | ||
|
|
95cc6ee2c0 | ||
|
|
db6fbdbf29 | ||
|
|
988d4b577e | ||
|
|
ec23716ee4 | ||
|
|
e6eb23eaac | ||
|
|
37ed639d1e | ||
|
|
a5aae5db9a | ||
|
|
6c9b84cc7a | ||
|
|
386b57bb77 | ||
|
|
521db6869e | ||
|
|
30d146e8b5 | ||
|
|
1891faa68d | ||
|
|
41456b69dd | ||
|
|
c5d9d57940 | ||
|
|
41f12e874d | ||
|
|
67bb446b5c | ||
|
|
972a4a471e | ||
|
|
7b387e479d | ||
|
|
b5979050f3 | ||
|
|
8276fdf605 | ||
|
|
d2d4fb9221 | ||
|
|
1bdf99109b | ||
|
|
e70fbf3204 | ||
|
|
4dc0aedbfe | ||
|
|
1c0cb24c7e | ||
|
|
4fd9182aff | ||
|
|
38cc916f7d | ||
|
|
51f354f5c2 | ||
|
|
cb24522dd5 | ||
|
|
cd8bb98c44 | ||
|
|
1be6f30188 | ||
|
|
2faa0891e4 | ||
|
|
3f03c93bc6 | ||
|
|
5ac4a9fac2 | ||
|
|
e46d55bc57 | ||
|
|
2c76506e07 | ||
|
|
dd5956be5e | ||
|
|
5dfe838c21 | ||
|
|
6aeea70ee0 | ||
|
|
6f7bad3f2f | ||
|
|
fbc88edf90 | ||
|
|
da5c80e113 | ||
|
|
09b1309c91 | ||
|
|
aee9ab36cb | ||
|
|
31e7ae8195 | ||
|
|
ba88cc239c | ||
|
|
90c4f50a5e | ||
|
|
3e5d4af411 | ||
|
|
ca6b192728 | ||
|
|
cb81e17fea | ||
|
|
1c7959d0de | ||
|
|
1d2dfab86d | ||
|
|
06e7d12640 | ||
|
|
ee332cee64 | ||
|
|
2e774a48b3 | ||
|
|
ce3272c41b | ||
|
|
3a683fff55 | ||
|
|
ff666be530 | ||
|
|
a008b63c16 | ||
|
|
c8d1e2e16f | ||
|
|
ebc1028771 | ||
|
|
fa2da0409e | ||
|
|
c81191b46a | ||
|
|
44093579e9 | ||
|
|
47ad29db3e | ||
|
|
33d694b89b | ||
|
|
ca0d3314cf | ||
|
|
63cef60594 | ||
|
|
7914433011 | ||
|
|
60013c5f10 | ||
|
|
48d58d72ff | ||
|
|
3e26dfa5bb | ||
|
|
851436c552 | ||
|
|
a3a1575e9d | ||
|
|
d0bdd7e771 | ||
|
|
cffea2979b | ||
|
|
5e9d75b863 | ||
|
|
4126c4c091 | ||
|
|
ef8275aede | ||
|
|
f8445a241b | ||
|
|
fde95fcd0f | ||
|
|
ebfaf2496b | ||
|
|
8e078534c2 | ||
|
|
a185705d19 | ||
|
|
003deac540 | ||
|
|
3f7beb4679 | ||
|
|
7905d02ce5 | ||
|
|
134a2c0bd3 | ||
|
|
ec413f7802 | ||
|
|
30371e64cd | ||
|
|
793f62357f | ||
|
|
d1613ef020 | ||
|
|
c5971a6fcb | ||
|
|
a8bfdc6f36 | ||
|
|
eba7289a8e | ||
|
|
61a80114eb | ||
|
|
8ffa2a31ff | ||
|
|
5a8d6c4e41 | ||
|
|
c05b2311d8 | ||
|
|
e31783e443 | ||
|
|
a978d56c7b | ||
|
|
81e7397770 | ||
|
|
bd62c6f66c | ||
|
|
6c93f01c97 | ||
|
|
6af622c340 | ||
|
|
4696ed7d0b | ||
|
|
b8b72e1b18 | ||
|
|
04a61ee6ad | ||
|
|
1f9eac4fbd | ||
|
|
0b9d44545b | ||
|
|
927fcbc1e3 | ||
|
|
14701ff070 | ||
|
|
e85878624a | ||
|
|
8b4c8823d9 | ||
|
|
d1dd512bef | ||
|
|
e51e61008a | ||
|
|
7b934d6888 | ||
|
|
a753e91e2c | ||
|
|
e5384ac13b | ||
|
|
0738eadcb5 | ||
|
|
ef3c770d95 | ||
|
|
9da816154e | ||
|
|
af3b6bbd68 | ||
|
|
49200d4747 | ||
|
|
addd5ef1dc | ||
|
|
de094fb63e | ||
|
|
442323928d | ||
|
|
64ab08e4aa | ||
|
|
0df490961b | ||
|
|
9e072cefec | ||
|
|
a319333161 | ||
|
|
29bea0a758 | ||
|
|
304fcb04e3 | ||
|
|
2707f2793d | ||
|
|
e5ad287843 | ||
|
|
106043fd9a | ||
|
|
3258f7c620 | ||
|
|
c401595e01 | ||
|
|
a6ecc202f0 | ||
|
|
53c8ab5b00 | ||
|
|
e625ce3b3b | ||
|
|
865128998c | ||
|
|
2ae61f3da0 |
@@ -16,6 +16,9 @@ jobs:
|
||||
run: |
|
||||
/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-runtime-resources-test.sh
|
||||
/bin/sh deploy/test-caddyfile-cache.sh
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -49,6 +49,7 @@ dockers:
|
||||
use: buildx
|
||||
extra_files:
|
||||
- deploy/docker-entrypoint.sh
|
||||
- backend/resources
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
|
||||
@@ -65,6 +65,7 @@ dockers:
|
||||
use: buildx
|
||||
extra_files:
|
||||
- deploy/docker-entrypoint.sh
|
||||
- backend/resources
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
@@ -80,6 +81,7 @@ dockers:
|
||||
use: buildx
|
||||
extra_files:
|
||||
- deploy/docker-entrypoint.sh
|
||||
- backend/resources
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
@@ -95,6 +97,7 @@ dockers:
|
||||
use: buildx
|
||||
extra_files:
|
||||
- deploy/docker-entrypoint.sh
|
||||
- backend/resources
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
@@ -110,6 +113,7 @@ dockers:
|
||||
use: buildx
|
||||
extra_files:
|
||||
- deploy/docker-entrypoint.sh
|
||||
- backend/resources
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
- "--label=org.opencontainers.image.version={{ .Version }}"
|
||||
|
||||
@@ -42,8 +42,9 @@ RUN addgroup -g 1000 sub2api && \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy pre-built binary from GoReleaser
|
||||
# Copy the pre-built binary and runtime fallback resources from GoReleaser
|
||||
COPY sub2api /app/sub2api
|
||||
COPY --chown=sub2api:sub2api backend/resources /app/resources
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data && chown -R sub2api:sub2api /app
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.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/views/auth/__tests__/LinuxDoCallbackView.spec.ts \
|
||||
src/views/auth/__tests__/WechatCallbackView.spec.ts \
|
||||
src/views/user/__tests__/PaymentView.spec.ts \
|
||||
|
||||
@@ -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,55 +91,12 @@ Please read the following carefully before using this project:
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://apikl.ai"><img src="assets/partners/logos/apikl.png" alt="apikl" width="150"></a></td>
|
||||
<td>Thanks to Apikl for sponsoring this project! Built on Sub2API, the platform provides developers with relay services for Codex / Claude series models, focusing on long-term stability, high-speed direct connections, and excellent cost-effectiveness. It offers pay-as-you-go balance billing, enterprise-grade official invoices, and one-on-one dedicated support. <a href="https://apikl.ai">Register now</a> for a 1:1 top-up bonus — double your balance!
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://tokeneum.ai"><img src="assets/partners/logos/tokeneum.png" alt="tokeneum" width="150"></a></td>
|
||||
<td>Thanks to TokenEum for sponsoring this project! <a href="https://tokeneum.ai">TokenEum</a> is a comprehensive AI model aggregation platform and intelligent agent development company. It brings together top-tier international models — including Claude, Gemini, and OpenAI — alongside leading open-source models such as GLM, Qwen, and Kimi, offering a wide range of options across different quality and price tiers to suit every need. TokenEum also provides access to cutting-edge video generation models like Seedance2.0 and Happy Horse. Committed to transparency and honest business practices, TokenEum ensures all model information is accurate and reliable. Visit <a href="https://tokeneum.ai">tokeneum.ai</a> to get started.
|
||||
</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://dis.chatdesks.cn/chatdesk/hsyqsub2api.html"><img src="assets/partners/logos/byteplus.png" alt="BytePlus" width="150"></a></td>
|
||||
<td>Thanks to Dola seed for sponsoring this project! Dola Seed 2.0 is a full‑modal general large model independently developed by ByteDance for the global market. Built on a unified multimodal architecture, it supports joint understanding and generation of text, images, audio, and video. It natively enables agent collaboration, with strong reasoning, long‑task execution, tool integration, and coding capabilities. It is widely applicable to smart cockpits, personal assistants, education, customer support, marketing, retail, and other scenarios. It excels in multimodal perception, end‑to‑end complex task delivery, stable interaction, and data security, and is readily accessible and deployable via the ModelArk platform.Register via <a href="https://dis.chatdesks.cn/chatdesk/hsyqsub2api.html">this link</a> to get 500,000 tokens of free inference quota per model.<a href="https://dis.chatdesks.cn/chatdesk/hsyqsub2api.html"> >>中国大陆地区的开发者请点击这里</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
|
||||
<td>Thanks to Suixiang AI Gateway for sponsoring this project! <a href="https://sui-xiang.com/">Suixiang AI Gateway</a> is a reliable and efficient API relay service provider offering relay services for Claude, Codex, Gemini, and more. A privacy-focused relay — no data reselling, no model dilution; privacy, transparency, and lightning-fast after-sales support. New accounts get ¥0.5 in trial credit daily by signing in; top-ups are 1:1, no subscription required, pay-as-you-go. Multi-line redundancy, cross-region disaster recovery, automatic failover, and uninterrupted long-link SSE. 99.9% availability — critical calls never fall behind.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.miyaip.com/?invitecode=sub2api"><img src="assets/partners/logos/miyaip.png" alt="miyaip" width="150"></a></td>
|
||||
<td>Thanks to MiyaIP for sponsoring this project! <a href="https://www.miyaip.com/?invitecode=sub2api">MiyaIP</a> is a platform dedicated to global residential proxy network services, committed to providing high-quality, pure overseas residential IP resources for enterprise developers, cross-border business teams, and AI application users. It delivers stable, independent overseas network environments for AI platforms, overseas SaaS, and other online services, supporting multi-region access testing and project environment isolation. Ideal for development and testing scenarios that require access to overseas AI services, such as: AI model platform access, AI development testing, AI SaaS service usage, AI API debugging, and multi-region network environment validation.
|
||||
</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.
|
||||
@@ -172,8 +118,8 @@ Model authenticity: no content intervention or secondary filtering — experienc
|
||||
</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 width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z"><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">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">exclusive link</a> and contact customer support to receive free trial credits; invoicing and team onboarding are also supported.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -183,6 +129,41 @@ Model authenticity: no content intervention or secondary filtering — experienc
|
||||
</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>
|
||||
|
||||
</table>
|
||||
|
||||
## Overview
|
||||
@@ -199,6 +180,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
|
||||
- **Rate Limiting** - Configurable request and token rate limits
|
||||
- **Built-in Payment System** - Supports EasyPay, Alipay, WeChat Pay, and Stripe for user self-service top-up, no separate payment service needed ([Configuration Guide](docs/PAYMENT.md))
|
||||
- **Admin Dashboard** - Web interface for monitoring and management
|
||||
- **Composite Groups** - Admin routing layer that resolves requested models to concrete providers for multi-provider groups ([Operator Guide](docs/COMPOSITE_GROUPS.md))
|
||||
- **External System Integration** - Embed external systems (e.g. ticketing) via iframe to extend the admin dashboard
|
||||
|
||||
## Ecosystem
|
||||
@@ -544,6 +526,7 @@ database:
|
||||
redis:
|
||||
host: "localhost"
|
||||
port: 6379
|
||||
username: ""
|
||||
password: ""
|
||||
|
||||
jwt:
|
||||
@@ -759,7 +742,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:
|
||||
|
||||
|
||||
@@ -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,55 +94,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://apikl.ai"><img src="assets/partners/logos/apikl.png" alt="apikl" width="150"></a></td>
|
||||
<td>感谢 Apikl 赞助本项目!平台基于 Sub2API 搭建,为开发者提供 Codex / Claude 系列模型的中转服务,专注于长期稳定、高速直连与高性价比。支持按量计费的余额结算、企业级正规发票及一对一专属对接。<a href="https://apikl.ai">立即注册</a>即享充值 1:1 赠送 — 余额翻倍!
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://tokeneum.ai"><img src="assets/partners/logos/tokeneum.png" alt="tokeneum" width="150"></a></td>
|
||||
<td>感谢 TokenEum 赞助本项目!<a href="https://tokeneum.ai">TokenEum</a> 是一家综合性 AI 模型聚合平台与智能体开发公司,汇聚 Claude、Gemini、OpenAI 等国际顶级模型,以及 GLM、Qwen、Kimi 等主流开源模型,提供不同质量与价格梯度的丰富选择,满足多样化需求。平台还接入了 Seedance2.0、Happy Horse 等前沿视频生成模型。秉持透明诚信的经营理念,TokenEum 确保所有模型信息真实可靠。访问 <a href="https://tokeneum.ai">tokeneum.ai</a> 开始使用。
|
||||
</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://dis.chatdesks.cn/chatdesk/hsyqsub2api.html"><img src="assets/partners/logos/huoshan.png" alt="HuoShan" width="150"></a></td>
|
||||
<td>感谢火山方舟 Agent Plan 模型赞助了本项目!方舟 Agent Plan 模型订阅套餐集成了包含 Doubao-Seed、Doubao-Seedance、Doubao-Seedream 等在内的字节跳动自研 SOTA 级模型,覆盖文本、代码、图像、视频等多模态任务。最新支持 MiniMax-M3、DeepSeek-V4 系列、GLM-5.1、Doubao-Seed-2.0 系列、Kimi-K2.6 等模型,工具不限。超全模态模型与 Harness 升级一步到位,深度支持 Agent 框架与 AI 编程工具。一次订阅,可以为不同任务切换合适的 AI 引擎。方舟 Coding Plan 为 Sub2Api 的用户提供了专属福利:通过<a href="https://dis.chatdesks.cn/chatdesk/hsyqsub2api.html">此链接</a>订阅方舟 Coding Plan,新客户首两个月享 2.5 折优惠 <a href="https://dis.chatdesks.cn/chatdesk/hsyqsub2api.html">>>For developers outside Mainland China, please click here</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
|
||||
<td>感谢 随想AI网关 赞助本项目!<a href="https://sui-xiang.com/">随想AI网关</a> 是一家可靠高效的 API 中继服务提供商,提供 Claude、Codex、Gemini 等的中继服务。注重隐私的中转站·无数据倒卖·无模型掺水,隐私,透明,极速售后。新账户注册每日签到就送 0.5 元测试额度,充值额度 1:1,无需订阅,按量付费。多线路冗余、跨区域容灾、自动故障切换,长链路 SSE 不中断。99.9% 可用性,关键调用从不掉队。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.miyaip.com/?invitecode=sub2api"><img src="assets/partners/logos/miyaip.png" alt="miyaip" width="150"></a></td>
|
||||
<td>感谢 MiyaIP 赞助本项目!<a href="https://www.miyaip.com/?invitecode=sub2api">MiyaIP</a> 是一家专注于全球住宅代理网络服务的平台,致力于为企业开发者、跨境业务团队及AI 应用用户提供高质量、纯净的海外住宅 IP 资源。为 AI 平台、海外 SaaS 及其他在线服务提供稳定、独立的海外网络环境,支持多地区访问测试和项目环境隔离。适用于需要访问海外 AI 服务的开发和测试场景,例如:AI 模型平台访问、AI 开发测试、AI SaaS 服务使用、AI API 调试、多地区网络环境验证
|
||||
</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>即可开始体验,轻松构建更稳定、高效的自动化工作流。
|
||||
@@ -175,8 +121,8 @@
|
||||
</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 width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z"><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">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">[专属链接]</a>注册后联系客服,可领取免费测试额度,并支持开票和团队对接。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -186,6 +132,41 @@
|
||||
</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>
|
||||
|
||||
</table>
|
||||
|
||||
## 项目概述
|
||||
|
||||
@@ -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,51 +93,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://apikl.ai"><img src="assets/partners/logos/apikl.png" alt="apikl" width="150"></a></td>
|
||||
<td>Apikl のご支援に感謝します!Sub2API をベースに構築された本プラットフォームは、開発者向けに Codex / Claude シリーズモデルの中継サービスを提供しています。長期安定性、高速直結、高いコストパフォーマンスを重視し、従量課金の残高ベース課金、エンタープライズグレードの正規請求書、1対1の専属サポートを提供します。<a href="https://apikl.ai">今すぐ登録</a>でチャージ 1:1 ボーナス — 残高が倍に!
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://tokeneum.ai"><img src="assets/partners/logos/tokeneum.png" alt="tokeneum" width="150"></a></td>
|
||||
<td>TokenEum のご支援に感謝します!<a href="https://tokeneum.ai">TokenEum</a> は総合的な AI モデル集約プラットフォームおよびインテリジェントエージェント開発企業です。Claude、Gemini、OpenAI などの世界トップクラスのモデルに加え、GLM、Qwen、Kimi などの主要なオープンソースモデルも集約しており、品質と価格の異なる豊富な選択肢を提供してあらゆるニーズに対応します。また、Seedance2.0 や Happy Horse などの最先端の動画生成モデルも利用可能です。TokenEum は透明性と誠実なビジネスを重視し、すべてのモデル情報の正確性と信頼性を保証します。<a href="https://tokeneum.ai">tokeneum.ai</a> でぜひお試しください。
|
||||
</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://dis.chatdesks.cn/chatdesk/hsyqsub2api.html"><img src="assets/partners/logos/byteplus.png" alt="BytePlus" width="150"></a></td>
|
||||
<td>Dola seed のご支援に感謝します!Dola Seed 2.0 は、ByteDance がグローバル市場向けに独自開発した全モーダル汎用大規模モデルです。統一されたマルチモーダルアーキテクチャに基づき、テキスト・画像・音声・動画の共同理解と生成をサポートします。エージェント協調をネイティブに有効化し、強力な推論・長期タスク実行・ツール統合・コーディング能力を備えています。スマートコックピット、パーソナルアシスタント、教育、カスタマーサポート、マーケティング、リテールなど多様なシナリオに広く適用可能です。マルチモーダル知覚、エンドツーエンドの複雑タスク配信、安定したインタラクション、データセキュリティに優れ、ModelArk プラットフォームを通じて容易にアクセス・デプロイできます。<a href="https://dis.chatdesks.cn/chatdesk/hsyqsub2api.html">こちらのリンク</a>から登録すると、各モデルにつき 50 万トークンの無料推論クォータを獲得できます。<a href="https://dis.chatdesks.cn/chatdesk/hsyqsub2api.html"> >>中国大陸地域の開発者はこちらをクリック</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://sui-xiang.com/"><img src="assets/partners/logos/sui-xiang.jpg" alt="sui-xiang" width="150"></a></td>
|
||||
<td>随想AI ゲートウェイのご支援に感謝します!<a href="https://sui-xiang.com/">随想AI ゲートウェイ</a> は信頼性と効率に優れた API 中継サービスプロバイダーで、Claude、Codex、Gemini などの中継サービスを提供しています。プライバシー重視の中継ステーション・データ転売なし・モデル水増しなし、プライバシー・透明性・超高速アフターサービス。新規アカウント登録後、毎日サインインで 0.5 元のテストクレジットがもらえ、チャージは 1:1、サブスクリプション不要、従量課金。マルチライン冗長、クロスリージョン災害復旧、自動フェイルオーバー、長時間 SSE 接続が途切れません。99.9% の可用性、重要な呼び出しは決して遅れません。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.miyaip.com/?invitecode=sub2api"><img src="assets/partners/logos/miyaip.png" alt="miyaip" width="150"></a></td>
|
||||
<td>MiyaIP のご支援に感謝します!<a href="https://www.miyaip.com/?invitecode=sub2api">MiyaIP</a> はグローバル住宅プロキシネットワークサービスに特化したプラットフォームで、企業開発者、越境ビジネスチーム、AI アプリケーションユーザーに高品質で純粋な海外住宅 IP リソースを提供することに注力しています。AI プラットフォーム、海外 SaaS、その他のオンラインサービスに安定した独立した海外ネットワーク環境を提供し、複数地域でのアクセステストやプロジェクト環境の分離をサポートします。海外 AI サービスへのアクセスが必要な開発・テストシナリオに最適です。例:AI モデルプラットフォームアクセス、AI 開発テスト、AI SaaS サービス利用、AI API デバッグ、複数地域のネットワーク環境検証など。
|
||||
</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>して、より安定した効率的な自動化ワークフローを簡単に構築しましょう。
|
||||
@@ -170,8 +120,8 @@
|
||||
</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 width="180"><a href="https://console.claudeapi.com/agent/register/drTKjyn6wGLK061Z"><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">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">専用リンク</a>から登録後カスタマーサポートへご連絡いただくと、無料お試しクレジットを受け取れます。請求書発行やチーム導入にも対応しています。
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -181,6 +131,41 @@
|
||||
</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>
|
||||
|
||||
</table>
|
||||
|
||||
## 概要
|
||||
|
||||
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,32 @@
|
||||
<svg
|
||||
width="340"
|
||||
height="112"
|
||||
viewBox="0 0 340 112"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label="Hao.ai"
|
||||
>
|
||||
<!-- Fill the compact landscape canvas while preserving the homepage lockup proportions. -->
|
||||
<g transform="translate(170 56) scale(1.45) translate(-170 -56)">
|
||||
<!-- Homepage HaoAI mark, rendered in the brand green. -->
|
||||
<g transform="translate(66.55 12) scale(0.55)">
|
||||
<path
|
||||
d="M49 30C42.3726 30 37 35.3726 37 42V118C37 124.627 42.3726 130 49 130C55.6274 130 61 124.627 61 118V42C61 35.3726 55.6274 30 49 30ZM111 30C104.373 30 99 35.3726 99 42V118C99 124.627 104.373 130 111 130C117.627 130 123 124.627 123 118V42C123 35.3726 117.627 30 111 30ZM80 92.5C86.9036 92.5 92.5 86.9036 92.5 80C92.5 73.0964 86.9036 67.5 80 67.5C73.0964 67.5 67.5 73.0964 67.5 80C67.5 86.9036 73.0964 92.5 80 92.5Z"
|
||||
fill="#1DB954"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<!-- Homepage wordmark converted from Inter SemiBold to vector outlines. -->
|
||||
<g transform="translate(150.35 69) scale(0.0166015625 -0.0166015625)" fill="#111111">
|
||||
<path d="M150 0L150 1490L417 1490L417 874L1110 874L1110 1490L1377 1490L1377 0L1110 0L1110 649L417 649L417 0Z" />
|
||||
<path transform="translate(1527 0)" d="M450 -23Q344 -23 259 15.5Q174 54 125 129Q76 204 76 314Q76 409 111.5 471Q147 533 207.5 570Q268 607 344 626Q420 645 502 653Q599 664 660 672Q721 680 749.5 698Q778 716 778 754L778 759Q778 814 756 852.5Q734 891 690.5 911.5Q647 932 583 932Q518 932 470.5 912Q423 892 393 860Q363 828 349 791L108 840Q141 939 210 1003.5Q279 1068 374.5 1100Q470 1132 582 1132Q661 1132 741.5 1113.5Q822 1095 889 1051Q956 1007 997 933Q1038 859 1038 747L1038 0L789 0L789 154L779 154Q754 107 710.5 66.5Q667 26 602.5 1.5Q538 -23 450 -23ZM517 170Q598 170 656.5 202Q715 234 747.5 286.5Q780 339 780 401L780 533Q767 523 736.5 514Q706 505 668.5 498Q631 491 594.5 486Q558 481 533 478Q475 470 428 452Q381 434 354.5 401.5Q328 369 328 317Q328 269 352.5 236.5Q377 204 419.5 187Q462 170 517 170Z" />
|
||||
<path transform="translate(2703 0)" d="M623 -23Q460 -23 340 49.5Q220 122 154.5 251.5Q89 381 89 553Q89 727 154.5 857.5Q220 988 340 1060Q460 1132 623 1132Q787 1132 907 1060Q1027 988 1092.5 857.5Q1158 727 1158 553Q1158 381 1092.5 251.5Q1027 122 907 49.5Q787 -23 623 -23ZM623 188Q714 188 774.5 237Q835 286 864.5 369Q894 452 894 554Q894 657 864.5 740Q835 823 774.5 872Q714 921 623 921Q532 921 472.5 872Q413 823 383.5 740.5Q354 658 354 554Q354 452 383.5 369Q413 286 472.5 237Q532 188 623 188Z" />
|
||||
<path transform="translate(3950 0)" fill="#1DB954" d="M326 -17Q259 -17 212.5 29Q166 75 166 143Q166 210 212.5 256Q259 302 326 302Q394 302 440.5 256Q487 210 487 143Q487 75 440.5 29Q394 -17 326 -17Z" />
|
||||
<path transform="translate(4603 0)" d="M450 -23Q344 -23 259 15.5Q174 54 125 129Q76 204 76 314Q76 409 111.5 471Q147 533 207.5 570Q268 607 344 626Q420 645 502 653Q599 664 660 672Q721 680 749.5 698Q778 716 778 754L778 759Q778 814 756 852.5Q734 891 690.5 911.5Q647 932 583 932Q518 932 470.5 912Q423 892 393 860Q363 828 349 791L108 840Q141 939 210 1003.5Q279 1068 374.5 1100Q470 1132 582 1132Q661 1132 741.5 1113.5Q822 1095 889 1051Q956 1007 997 933Q1038 859 1038 747L1038 0L789 0L789 154L779 154Q754 107 710.5 66.5Q667 26 602.5 1.5Q538 -23 450 -23ZM517 170Q598 170 656.5 202Q715 234 747.5 286.5Q780 339 780 401L780 533Q767 523 736.5 514Q706 505 668.5 498Q631 491 594.5 486Q558 481 533 478Q475 470 428 452Q381 434 354.5 401.5Q328 369 328 317Q328 269 352.5 236.5Q377 204 419.5 187Q462 170 517 170Z" />
|
||||
<path transform="translate(5779 0)" d="M138 0L138 1118L398 1118L398 0ZM268 1276Q206 1276 161.5 1318Q117 1360 117 1418Q117 1478 161.5 1519.5Q206 1561 268 1561Q331 1561 375.5 1519.5Q420 1478 420 1419Q420 1360 375.5 1318Q331 1276 268 1276Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 744 KiB |
@@ -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.161
|
||||
0.1.171
|
||||
|
||||
@@ -183,7 +183,7 @@ func runMainServer() {
|
||||
defer cancel()
|
||||
|
||||
if err := app.Server.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
log.Printf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Server exited")
|
||||
|
||||
@@ -88,6 +88,7 @@ func provideCleanup(
|
||||
schedulerSnapshot *service.SchedulerSnapshotService,
|
||||
tokenRefresh *service.TokenRefreshService,
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
codexVersionSync *service.OpenAICodexVersionSyncService,
|
||||
proxyExpiry *service.ProxyExpiryService,
|
||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||
usageCleanup *service.UsageCleanupService,
|
||||
@@ -111,6 +112,7 @@ func provideCleanup(
|
||||
channelMonitorRunner *service.ChannelMonitorRunner,
|
||||
quotaFlusher *service.UserPlatformQuotaUsageFlusher,
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService,
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService,
|
||||
auditLog *service.AuditLogService,
|
||||
promptAudit *securityaudit.PromptService,
|
||||
) func() {
|
||||
@@ -235,6 +237,10 @@ func provideCleanup(
|
||||
accountExpiry.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"OpenAICodexVersionSyncService", func() error {
|
||||
codexVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ProxyExpiryService", func() error {
|
||||
proxyExpiry.Stop()
|
||||
return nil
|
||||
@@ -331,6 +337,12 @@ func provideCleanup(
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OllamaCloudUsageService", func() error {
|
||||
if ollamaCloudUsage != nil {
|
||||
ollamaCloudUsage.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
}
|
||||
|
||||
infraSteps := []cleanupStep{
|
||||
|
||||
@@ -57,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)
|
||||
@@ -78,7 +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)
|
||||
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)
|
||||
@@ -135,9 +139,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
channelRepository := repository.NewChannelRepository(db)
|
||||
channelService := service.NewChannelService(channelRepository, groupRepository, apiKeyAuthCacheInvalidator, pricingService)
|
||||
modelPricingResolver := service.NewModelPricingResolver(channelService, billingService)
|
||||
compositeModelRouteRepository := repository.NewCompositeModelRouteRepository(client)
|
||||
compositeRouteResolver := service.NewCompositeRouteResolver(compositeModelRouteRepository)
|
||||
notificationEmailService := service.NewNotificationEmailService(settingRepository, emailService)
|
||||
balanceNotifyService := service.ProvideBalanceNotifyService(emailService, settingRepository, accountRepository, notificationEmailService)
|
||||
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService, tlsFingerprintProfileService, channelService, modelPricingResolver, balanceNotifyService, serviceUserPlatformQuotaRepository)
|
||||
gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, usageBillingRepository, userRepository, userSubscriptionRepository, userGroupRateRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache, rpmCache, digestSessionStore, settingService, tlsFingerprintProfileService, channelService, modelPricingResolver, compositeRouteResolver, balanceNotifyService, serviceUserPlatformQuotaRepository)
|
||||
openAIOAuthClient := repository.NewOpenAIOAuthClient()
|
||||
privacyClientFactory := providePrivacyClientFactory()
|
||||
openAIOAuthService := service.ProvideOpenAIOAuthService(proxyRepository, openAIOAuthClient, privacyClientFactory)
|
||||
@@ -180,7 +186,7 @@ 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)
|
||||
adminService := service.NewAdminService(userRepository, adminGroupRepository, adminAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, userRPMCache, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, openAIGatewayService, affiliateService, compositeModelRouteRepository, compositeRouteResolver)
|
||||
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService, serviceUserPlatformQuotaRepository, billingCache, totpService, userService, settingService)
|
||||
groupCapacityService := service.NewGroupCapacityService(accountRepository, groupRepository, concurrencyService, sessionLimitCache, rpmCache)
|
||||
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService)
|
||||
@@ -204,7 +210,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
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)
|
||||
@@ -220,7 +226,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)
|
||||
@@ -251,9 +257,9 @@ 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)
|
||||
configManager := securityaudit.NewConfigManager(db, settingRepository, redisClient, secretEncryptor, configConfig)
|
||||
postgreSQLRepository := securityaudit.NewPostgreSQLRepository(db)
|
||||
redisPayloadStore := securityaudit.NewRedisPayloadStore(redisClient)
|
||||
openAICompatibleScanner := securityaudit.NewOpenAICompatibleScanner()
|
||||
@@ -267,7 +273,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
auditLogService := service.ProvideAuditLogService(auditLogRepository, settingService)
|
||||
auditLogHandler := admin.NewAuditLogHandler(auditLogService, totpService)
|
||||
upstreamBillingProbeService := service.ProvideUpstreamBillingProbeService(accountRepository, accountTestService, settingService, leaderLockCache, db)
|
||||
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 := 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)
|
||||
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
||||
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
|
||||
@@ -277,9 +284,17 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
openAIGatewayHandler := handler.ProvideOpenAIGatewayHandler(openAIGatewayService, 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)
|
||||
imageTaskStore := repository.NewImageTaskStore(redisClient)
|
||||
imageTaskService := service.ProvideImageTaskService(imageTaskStore, imageStorageSettingService)
|
||||
asyncImageHandler := handler.NewAsyncImageHandler(imageTaskService, openAIGatewayHandler)
|
||||
@@ -293,13 +308,14 @@ 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, handlerPaymentHandler, paymentWebhookHandler, availableChannelHandler, asyncImageHandler, batchImageHandler, idempotencyCoordinator, idempotencyCleanupService)
|
||||
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)
|
||||
jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService, settingService, auditLogService)
|
||||
optionalJWTAuthMiddleware := middleware.NewOptionalJWTAuthMiddleware(authService, userService, settingService, auditLogService)
|
||||
adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService, auditLogService)
|
||||
apiKeyAuthMiddleware := middleware.NewAPIKeyAuthMiddleware(apiKeyService, subscriptionService, configConfig)
|
||||
auditLogMiddleware := middleware.NewAuditLogMiddleware(auditLogService)
|
||||
stepUpAuthMiddleware := middleware.NewStepUpAuthMiddleware(totpService, userService, settingService)
|
||||
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, auditLogMiddleware, stepUpAuthMiddleware, apiKeyService, subscriptionService, opsService, settingService, redisClient)
|
||||
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, optionalJWTAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, auditLogMiddleware, stepUpAuthMiddleware, apiKeyService, subscriptionService, opsService, settingService, compositeRouteResolver, redisClient)
|
||||
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
||||
opsMetricsCollector := service.ProvideOpsMetricsCollector(opsRepository, settingRepository, accountRepository, concurrencyService, db, redisClient, configConfig)
|
||||
opsAggregationService := service.ProvideOpsAggregationService(opsRepository, settingRepository, db, redisClient, configConfig)
|
||||
@@ -308,6 +324,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
|
||||
opsIngressRejectAggregator := service.ProvideOpsIngressRejectAggregator(opsRepository, opsService)
|
||||
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
||||
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)
|
||||
@@ -315,7 +332,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService, leaderLockCache, db)
|
||||
channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService, 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, auditLogService, promptService)
|
||||
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, opsService, opsIngressRejectAggregator, apiKeyService, authCacheInvalidationWorker, schedulerSnapshotService, tokenRefreshService, accountExpiryService, openAICodexVersionSyncService, 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)
|
||||
application := &Application{
|
||||
Server: httpServer,
|
||||
PromptAudit: promptService,
|
||||
@@ -359,6 +376,7 @@ func provideCleanup(
|
||||
schedulerSnapshot *service.SchedulerSnapshotService,
|
||||
tokenRefresh *service.TokenRefreshService,
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
codexVersionSync *service.OpenAICodexVersionSyncService,
|
||||
proxyExpiry *service.ProxyExpiryService,
|
||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||
usageCleanup *service.UsageCleanupService,
|
||||
@@ -382,6 +400,7 @@ func provideCleanup(
|
||||
channelMonitorRunner *service.ChannelMonitorRunner,
|
||||
quotaFlusher *service.UserPlatformQuotaUsageFlusher,
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService,
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService,
|
||||
auditLog *service.AuditLogService,
|
||||
promptAudit *securityaudit.PromptService,
|
||||
) func() {
|
||||
@@ -505,6 +524,10 @@ func provideCleanup(
|
||||
accountExpiry.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"OpenAICodexVersionSyncService", func() error {
|
||||
codexVersionSync.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"ProxyExpiryService", func() error {
|
||||
proxyExpiry.Stop()
|
||||
return nil
|
||||
@@ -601,6 +624,12 @@ func provideCleanup(
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
{"OllamaCloudUsageService", func() error {
|
||||
if ollamaCloudUsage != nil {
|
||||
ollamaCloudUsage.Stop()
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
}
|
||||
|
||||
infraSteps := []cleanupStep{
|
||||
|
||||
@@ -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,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
schedulerSnapshotSvc,
|
||||
tokenRefreshSvc,
|
||||
accountExpirySvc,
|
||||
codexVersionSyncSvc,
|
||||
proxyExpirySvc,
|
||||
subscriptionExpirySvc,
|
||||
&service.UsageCleanupService{},
|
||||
@@ -88,6 +90,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
|
||||
nil, // channelMonitorRunner
|
||||
nil, // quotaFlusher
|
||||
nil, // upstreamBillingProbe
|
||||
nil, // ollamaCloudUsage
|
||||
nil, // auditLog
|
||||
nil, // promptAudit
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
|
||||
@@ -90,6 +91,8 @@ type Client struct {
|
||||
ChannelMonitorHistory *ChannelMonitorHistoryClient
|
||||
// ChannelMonitorRequestTemplate is the client for interacting with the ChannelMonitorRequestTemplate builders.
|
||||
ChannelMonitorRequestTemplate *ChannelMonitorRequestTemplateClient
|
||||
// CompositeModelRoute is the client for interacting with the CompositeModelRoute builders.
|
||||
CompositeModelRoute *CompositeModelRouteClient
|
||||
// ErrorPassthroughRule is the client for interacting with the ErrorPassthroughRule builders.
|
||||
ErrorPassthroughRule *ErrorPassthroughRuleClient
|
||||
// Group is the client for interacting with the Group builders.
|
||||
@@ -163,6 +166,7 @@ func (c *Client) init() {
|
||||
c.ChannelMonitorDailyRollup = NewChannelMonitorDailyRollupClient(c.config)
|
||||
c.ChannelMonitorHistory = NewChannelMonitorHistoryClient(c.config)
|
||||
c.ChannelMonitorRequestTemplate = NewChannelMonitorRequestTemplateClient(c.config)
|
||||
c.CompositeModelRoute = NewCompositeModelRouteClient(c.config)
|
||||
c.ErrorPassthroughRule = NewErrorPassthroughRuleClient(c.config)
|
||||
c.Group = NewGroupClient(c.config)
|
||||
c.IdempotencyRecord = NewIdempotencyRecordClient(c.config)
|
||||
@@ -293,6 +297,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
|
||||
ChannelMonitorDailyRollup: NewChannelMonitorDailyRollupClient(cfg),
|
||||
ChannelMonitorHistory: NewChannelMonitorHistoryClient(cfg),
|
||||
ChannelMonitorRequestTemplate: NewChannelMonitorRequestTemplateClient(cfg),
|
||||
CompositeModelRoute: NewCompositeModelRouteClient(cfg),
|
||||
ErrorPassthroughRule: NewErrorPassthroughRuleClient(cfg),
|
||||
Group: NewGroupClient(cfg),
|
||||
IdempotencyRecord: NewIdempotencyRecordClient(cfg),
|
||||
@@ -350,6 +355,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
|
||||
ChannelMonitorDailyRollup: NewChannelMonitorDailyRollupClient(cfg),
|
||||
ChannelMonitorHistory: NewChannelMonitorHistoryClient(cfg),
|
||||
ChannelMonitorRequestTemplate: NewChannelMonitorRequestTemplateClient(cfg),
|
||||
CompositeModelRoute: NewCompositeModelRouteClient(cfg),
|
||||
ErrorPassthroughRule: NewErrorPassthroughRuleClient(cfg),
|
||||
Group: NewGroupClient(cfg),
|
||||
IdempotencyRecord: NewIdempotencyRecordClient(cfg),
|
||||
@@ -407,7 +413,7 @@ func (c *Client) Use(hooks ...Hook) {
|
||||
c.AuthIdentity, c.AuthIdentityChannel, c.BatchImageEvent, c.BatchImageItem,
|
||||
c.BatchImageJob, c.ChannelMonitor, c.ChannelMonitorDailyRollup,
|
||||
c.ChannelMonitorHistory, c.ChannelMonitorRequestTemplate,
|
||||
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord,
|
||||
c.CompositeModelRoute, c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord,
|
||||
c.IdentityAdoptionDecision, c.PaymentAuditLog, c.PaymentOrder,
|
||||
c.PaymentProviderInstance, c.PendingAuthSession, c.PromoCode, c.PromoCodeUsage,
|
||||
c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting, c.SubscriptionPlan,
|
||||
@@ -427,7 +433,7 @@ func (c *Client) Intercept(interceptors ...Interceptor) {
|
||||
c.AuthIdentity, c.AuthIdentityChannel, c.BatchImageEvent, c.BatchImageItem,
|
||||
c.BatchImageJob, c.ChannelMonitor, c.ChannelMonitorDailyRollup,
|
||||
c.ChannelMonitorHistory, c.ChannelMonitorRequestTemplate,
|
||||
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord,
|
||||
c.CompositeModelRoute, c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord,
|
||||
c.IdentityAdoptionDecision, c.PaymentAuditLog, c.PaymentOrder,
|
||||
c.PaymentProviderInstance, c.PendingAuthSession, c.PromoCode, c.PromoCodeUsage,
|
||||
c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting, c.SubscriptionPlan,
|
||||
@@ -470,6 +476,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
|
||||
return c.ChannelMonitorHistory.mutate(ctx, m)
|
||||
case *ChannelMonitorRequestTemplateMutation:
|
||||
return c.ChannelMonitorRequestTemplate.mutate(ctx, m)
|
||||
case *CompositeModelRouteMutation:
|
||||
return c.CompositeModelRoute.mutate(ctx, m)
|
||||
case *ErrorPassthroughRuleMutation:
|
||||
return c.ErrorPassthroughRule.mutate(ctx, m)
|
||||
case *GroupMutation:
|
||||
@@ -2724,6 +2732,157 @@ func (c *ChannelMonitorRequestTemplateClient) mutate(ctx context.Context, m *Cha
|
||||
}
|
||||
}
|
||||
|
||||
// CompositeModelRouteClient is a client for the CompositeModelRoute schema.
|
||||
type CompositeModelRouteClient struct {
|
||||
config
|
||||
}
|
||||
|
||||
// NewCompositeModelRouteClient returns a client for the CompositeModelRoute from the given config.
|
||||
func NewCompositeModelRouteClient(c config) *CompositeModelRouteClient {
|
||||
return &CompositeModelRouteClient{config: c}
|
||||
}
|
||||
|
||||
// Use adds a list of mutation hooks to the hooks stack.
|
||||
// A call to `Use(f, g, h)` equals to `compositemodelroute.Hooks(f(g(h())))`.
|
||||
func (c *CompositeModelRouteClient) Use(hooks ...Hook) {
|
||||
c.hooks.CompositeModelRoute = append(c.hooks.CompositeModelRoute, hooks...)
|
||||
}
|
||||
|
||||
// Intercept adds a list of query interceptors to the interceptors stack.
|
||||
// A call to `Intercept(f, g, h)` equals to `compositemodelroute.Intercept(f(g(h())))`.
|
||||
func (c *CompositeModelRouteClient) Intercept(interceptors ...Interceptor) {
|
||||
c.inters.CompositeModelRoute = append(c.inters.CompositeModelRoute, interceptors...)
|
||||
}
|
||||
|
||||
// Create returns a builder for creating a CompositeModelRoute entity.
|
||||
func (c *CompositeModelRouteClient) Create() *CompositeModelRouteCreate {
|
||||
mutation := newCompositeModelRouteMutation(c.config, OpCreate)
|
||||
return &CompositeModelRouteCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// CreateBulk returns a builder for creating a bulk of CompositeModelRoute entities.
|
||||
func (c *CompositeModelRouteClient) CreateBulk(builders ...*CompositeModelRouteCreate) *CompositeModelRouteCreateBulk {
|
||||
return &CompositeModelRouteCreateBulk{config: c.config, builders: builders}
|
||||
}
|
||||
|
||||
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
|
||||
// a builder and applies setFunc on it.
|
||||
func (c *CompositeModelRouteClient) MapCreateBulk(slice any, setFunc func(*CompositeModelRouteCreate, int)) *CompositeModelRouteCreateBulk {
|
||||
rv := reflect.ValueOf(slice)
|
||||
if rv.Kind() != reflect.Slice {
|
||||
return &CompositeModelRouteCreateBulk{err: fmt.Errorf("calling to CompositeModelRouteClient.MapCreateBulk with wrong type %T, need slice", slice)}
|
||||
}
|
||||
builders := make([]*CompositeModelRouteCreate, rv.Len())
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
builders[i] = c.Create()
|
||||
setFunc(builders[i], i)
|
||||
}
|
||||
return &CompositeModelRouteCreateBulk{config: c.config, builders: builders}
|
||||
}
|
||||
|
||||
// Update returns an update builder for CompositeModelRoute.
|
||||
func (c *CompositeModelRouteClient) Update() *CompositeModelRouteUpdate {
|
||||
mutation := newCompositeModelRouteMutation(c.config, OpUpdate)
|
||||
return &CompositeModelRouteUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// UpdateOne returns an update builder for the given entity.
|
||||
func (c *CompositeModelRouteClient) UpdateOne(_m *CompositeModelRoute) *CompositeModelRouteUpdateOne {
|
||||
mutation := newCompositeModelRouteMutation(c.config, OpUpdateOne, withCompositeModelRoute(_m))
|
||||
return &CompositeModelRouteUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// UpdateOneID returns an update builder for the given id.
|
||||
func (c *CompositeModelRouteClient) UpdateOneID(id int64) *CompositeModelRouteUpdateOne {
|
||||
mutation := newCompositeModelRouteMutation(c.config, OpUpdateOne, withCompositeModelRouteID(id))
|
||||
return &CompositeModelRouteUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// Delete returns a delete builder for CompositeModelRoute.
|
||||
func (c *CompositeModelRouteClient) Delete() *CompositeModelRouteDelete {
|
||||
mutation := newCompositeModelRouteMutation(c.config, OpDelete)
|
||||
return &CompositeModelRouteDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||
}
|
||||
|
||||
// DeleteOne returns a builder for deleting the given entity.
|
||||
func (c *CompositeModelRouteClient) DeleteOne(_m *CompositeModelRoute) *CompositeModelRouteDeleteOne {
|
||||
return c.DeleteOneID(_m.ID)
|
||||
}
|
||||
|
||||
// DeleteOneID returns a builder for deleting the given entity by its id.
|
||||
func (c *CompositeModelRouteClient) DeleteOneID(id int64) *CompositeModelRouteDeleteOne {
|
||||
builder := c.Delete().Where(compositemodelroute.ID(id))
|
||||
builder.mutation.id = &id
|
||||
builder.mutation.op = OpDeleteOne
|
||||
return &CompositeModelRouteDeleteOne{builder}
|
||||
}
|
||||
|
||||
// Query returns a query builder for CompositeModelRoute.
|
||||
func (c *CompositeModelRouteClient) Query() *CompositeModelRouteQuery {
|
||||
return &CompositeModelRouteQuery{
|
||||
config: c.config,
|
||||
ctx: &QueryContext{Type: TypeCompositeModelRoute},
|
||||
inters: c.Interceptors(),
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns a CompositeModelRoute entity by its id.
|
||||
func (c *CompositeModelRouteClient) Get(ctx context.Context, id int64) (*CompositeModelRoute, error) {
|
||||
return c.Query().Where(compositemodelroute.ID(id)).Only(ctx)
|
||||
}
|
||||
|
||||
// GetX is like Get, but panics if an error occurs.
|
||||
func (c *CompositeModelRouteClient) GetX(ctx context.Context, id int64) *CompositeModelRoute {
|
||||
obj, err := c.Get(ctx, id)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// QueryGroup queries the group edge of a CompositeModelRoute.
|
||||
func (c *CompositeModelRouteClient) QueryGroup(_m *CompositeModelRoute) *GroupQuery {
|
||||
query := (&GroupClient{config: c.config}).Query()
|
||||
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
|
||||
id := _m.ID
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(compositemodelroute.Table, compositemodelroute.FieldID, id),
|
||||
sqlgraph.To(group.Table, group.FieldID),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, compositemodelroute.GroupTable, compositemodelroute.GroupColumn),
|
||||
)
|
||||
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
|
||||
return fromV, nil
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// Hooks returns the client hooks.
|
||||
func (c *CompositeModelRouteClient) Hooks() []Hook {
|
||||
hooks := c.hooks.CompositeModelRoute
|
||||
return append(hooks[:len(hooks):len(hooks)], compositemodelroute.Hooks[:]...)
|
||||
}
|
||||
|
||||
// Interceptors returns the client interceptors.
|
||||
func (c *CompositeModelRouteClient) Interceptors() []Interceptor {
|
||||
inters := c.inters.CompositeModelRoute
|
||||
return append(inters[:len(inters):len(inters)], compositemodelroute.Interceptors[:]...)
|
||||
}
|
||||
|
||||
func (c *CompositeModelRouteClient) mutate(ctx context.Context, m *CompositeModelRouteMutation) (Value, error) {
|
||||
switch m.Op() {
|
||||
case OpCreate:
|
||||
return (&CompositeModelRouteCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||
case OpUpdate:
|
||||
return (&CompositeModelRouteUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||
case OpUpdateOne:
|
||||
return (&CompositeModelRouteUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||
case OpDelete, OpDeleteOne:
|
||||
return (&CompositeModelRouteDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
|
||||
default:
|
||||
return nil, fmt.Errorf("ent: unknown CompositeModelRoute mutation op: %q", m.Op())
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorPassthroughRuleClient is a client for the ErrorPassthroughRule schema.
|
||||
type ErrorPassthroughRuleClient struct {
|
||||
config
|
||||
@@ -6669,23 +6828,25 @@ type (
|
||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead, AuthIdentity,
|
||||
AuthIdentityChannel, BatchImageEvent, BatchImageItem, BatchImageJob,
|
||||
ChannelMonitor, ChannelMonitorDailyRollup, ChannelMonitorHistory,
|
||||
ChannelMonitorRequestTemplate, 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, 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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
|
||||
package ent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
)
|
||||
|
||||
// CompositeModelRoute is the model entity for the CompositeModelRoute schema.
|
||||
type CompositeModelRoute struct {
|
||||
config `json:"-"`
|
||||
// ID of the ent.
|
||||
ID int64 `json:"id,omitempty"`
|
||||
// CreatedAt holds the value of the "created_at" field.
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
// UpdatedAt holds the value of the "updated_at" field.
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
// DeletedAt holds the value of the "deleted_at" field.
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
// GroupID holds the value of the "group_id" field.
|
||||
GroupID int64 `json:"group_id,omitempty"`
|
||||
// Client-facing model identifier or prefix.
|
||||
PublicModel string `json:"public_model,omitempty"`
|
||||
// exact or prefix.
|
||||
MatchType string `json:"match_type,omitempty"`
|
||||
// Concrete provider platform.
|
||||
TargetPlatform string `json:"target_platform,omitempty"`
|
||||
// Provider model identifier; empty means public_model.
|
||||
UpstreamModel string `json:"upstream_model,omitempty"`
|
||||
// Endpoint scope such as any, messages, responses, chat_completions.
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
// Lower values win within the same match strength.
|
||||
Priority int `json:"priority,omitempty"`
|
||||
// Enabled holds the value of the "enabled" field.
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
// Notes holds the value of the "notes" field.
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the CompositeModelRouteQuery when eager-loading is set.
|
||||
Edges CompositeModelRouteEdges `json:"edges"`
|
||||
selectValues sql.SelectValues
|
||||
}
|
||||
|
||||
// CompositeModelRouteEdges holds the relations/edges for other nodes in the graph.
|
||||
type CompositeModelRouteEdges struct {
|
||||
// Group holds the value of the group edge.
|
||||
Group *Group `json:"group,omitempty"`
|
||||
// loadedTypes holds the information for reporting if a
|
||||
// type was loaded (or requested) in eager-loading or not.
|
||||
loadedTypes [1]bool
|
||||
}
|
||||
|
||||
// GroupOrErr returns the Group value or an error if the edge
|
||||
// was not loaded in eager-loading, or loaded but was not found.
|
||||
func (e CompositeModelRouteEdges) GroupOrErr() (*Group, error) {
|
||||
if e.Group != nil {
|
||||
return e.Group, nil
|
||||
} else if e.loadedTypes[0] {
|
||||
return nil, &NotFoundError{label: group.Label}
|
||||
}
|
||||
return nil, &NotLoadedError{edge: "group"}
|
||||
}
|
||||
|
||||
// scanValues returns the types for scanning values from sql.Rows.
|
||||
func (*CompositeModelRoute) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case compositemodelroute.FieldEnabled:
|
||||
values[i] = new(sql.NullBool)
|
||||
case compositemodelroute.FieldID, compositemodelroute.FieldGroupID, compositemodelroute.FieldPriority:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case compositemodelroute.FieldPublicModel, compositemodelroute.FieldMatchType, compositemodelroute.FieldTargetPlatform, compositemodelroute.FieldUpstreamModel, compositemodelroute.FieldEndpoint, compositemodelroute.FieldNotes:
|
||||
values[i] = new(sql.NullString)
|
||||
case compositemodelroute.FieldCreatedAt, compositemodelroute.FieldUpdatedAt, compositemodelroute.FieldDeletedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
default:
|
||||
values[i] = new(sql.UnknownType)
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// assignValues assigns the values that were returned from sql.Rows (after scanning)
|
||||
// to the CompositeModelRoute fields.
|
||||
func (_m *CompositeModelRoute) assignValues(columns []string, values []any) error {
|
||||
if m, n := len(values), len(columns); m < n {
|
||||
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
|
||||
}
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case compositemodelroute.FieldID:
|
||||
value, ok := values[i].(*sql.NullInt64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field id", value)
|
||||
}
|
||||
_m.ID = int64(value.Int64)
|
||||
case compositemodelroute.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
||||
} else if value.Valid {
|
||||
_m.CreatedAt = value.Time
|
||||
}
|
||||
case compositemodelroute.FieldUpdatedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field updated_at", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UpdatedAt = value.Time
|
||||
}
|
||||
case compositemodelroute.FieldDeletedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field deleted_at", values[i])
|
||||
} else if value.Valid {
|
||||
_m.DeletedAt = new(time.Time)
|
||||
*_m.DeletedAt = value.Time
|
||||
}
|
||||
case compositemodelroute.FieldGroupID:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field group_id", values[i])
|
||||
} else if value.Valid {
|
||||
_m.GroupID = value.Int64
|
||||
}
|
||||
case compositemodelroute.FieldPublicModel:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field public_model", values[i])
|
||||
} else if value.Valid {
|
||||
_m.PublicModel = value.String
|
||||
}
|
||||
case compositemodelroute.FieldMatchType:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field match_type", values[i])
|
||||
} else if value.Valid {
|
||||
_m.MatchType = value.String
|
||||
}
|
||||
case compositemodelroute.FieldTargetPlatform:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field target_platform", values[i])
|
||||
} else if value.Valid {
|
||||
_m.TargetPlatform = value.String
|
||||
}
|
||||
case compositemodelroute.FieldUpstreamModel:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field upstream_model", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UpstreamModel = value.String
|
||||
}
|
||||
case compositemodelroute.FieldEndpoint:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field endpoint", values[i])
|
||||
} else if value.Valid {
|
||||
_m.Endpoint = value.String
|
||||
}
|
||||
case compositemodelroute.FieldPriority:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field priority", values[i])
|
||||
} else if value.Valid {
|
||||
_m.Priority = int(value.Int64)
|
||||
}
|
||||
case compositemodelroute.FieldEnabled:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field enabled", values[i])
|
||||
} else if value.Valid {
|
||||
_m.Enabled = value.Bool
|
||||
}
|
||||
case compositemodelroute.FieldNotes:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field notes", values[i])
|
||||
} else if value.Valid {
|
||||
_m.Notes = new(string)
|
||||
*_m.Notes = value.String
|
||||
}
|
||||
default:
|
||||
_m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value returns the ent.Value that was dynamically selected and assigned to the CompositeModelRoute.
|
||||
// This includes values selected through modifiers, order, etc.
|
||||
func (_m *CompositeModelRoute) Value(name string) (ent.Value, error) {
|
||||
return _m.selectValues.Get(name)
|
||||
}
|
||||
|
||||
// QueryGroup queries the "group" edge of the CompositeModelRoute entity.
|
||||
func (_m *CompositeModelRoute) QueryGroup() *GroupQuery {
|
||||
return NewCompositeModelRouteClient(_m.config).QueryGroup(_m)
|
||||
}
|
||||
|
||||
// Update returns a builder for updating this CompositeModelRoute.
|
||||
// Note that you need to call CompositeModelRoute.Unwrap() before calling this method if this CompositeModelRoute
|
||||
// was returned from a transaction, and the transaction was committed or rolled back.
|
||||
func (_m *CompositeModelRoute) Update() *CompositeModelRouteUpdateOne {
|
||||
return NewCompositeModelRouteClient(_m.config).UpdateOne(_m)
|
||||
}
|
||||
|
||||
// Unwrap unwraps the CompositeModelRoute entity that was returned from a transaction after it was closed,
|
||||
// so that all future queries will be executed through the driver which created the transaction.
|
||||
func (_m *CompositeModelRoute) Unwrap() *CompositeModelRoute {
|
||||
_tx, ok := _m.config.driver.(*txDriver)
|
||||
if !ok {
|
||||
panic("ent: CompositeModelRoute is not a transactional entity")
|
||||
}
|
||||
_m.config.driver = _tx.drv
|
||||
return _m
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer.
|
||||
func (_m *CompositeModelRoute) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("CompositeModelRoute(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("updated_at=")
|
||||
builder.WriteString(_m.UpdatedAt.Format(time.ANSIC))
|
||||
builder.WriteString(", ")
|
||||
if v := _m.DeletedAt; v != nil {
|
||||
builder.WriteString("deleted_at=")
|
||||
builder.WriteString(v.Format(time.ANSIC))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("group_id=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.GroupID))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("public_model=")
|
||||
builder.WriteString(_m.PublicModel)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("match_type=")
|
||||
builder.WriteString(_m.MatchType)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("target_platform=")
|
||||
builder.WriteString(_m.TargetPlatform)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("upstream_model=")
|
||||
builder.WriteString(_m.UpstreamModel)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("endpoint=")
|
||||
builder.WriteString(_m.Endpoint)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("priority=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Priority))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("enabled=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.Enabled))
|
||||
builder.WriteString(", ")
|
||||
if v := _m.Notes; v != nil {
|
||||
builder.WriteString("notes=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// CompositeModelRoutes is a parsable slice of CompositeModelRoute.
|
||||
type CompositeModelRoutes []*CompositeModelRoute
|
||||
@@ -0,0 +1,200 @@
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
|
||||
package compositemodelroute
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
)
|
||||
|
||||
const (
|
||||
// Label holds the string label denoting the compositemodelroute type in the database.
|
||||
Label = "composite_model_route"
|
||||
// FieldID holds the string denoting the id field in the database.
|
||||
FieldID = "id"
|
||||
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
||||
FieldCreatedAt = "created_at"
|
||||
// FieldUpdatedAt holds the string denoting the updated_at field in the database.
|
||||
FieldUpdatedAt = "updated_at"
|
||||
// FieldDeletedAt holds the string denoting the deleted_at field in the database.
|
||||
FieldDeletedAt = "deleted_at"
|
||||
// FieldGroupID holds the string denoting the group_id field in the database.
|
||||
FieldGroupID = "group_id"
|
||||
// FieldPublicModel holds the string denoting the public_model field in the database.
|
||||
FieldPublicModel = "public_model"
|
||||
// FieldMatchType holds the string denoting the match_type field in the database.
|
||||
FieldMatchType = "match_type"
|
||||
// FieldTargetPlatform holds the string denoting the target_platform field in the database.
|
||||
FieldTargetPlatform = "target_platform"
|
||||
// FieldUpstreamModel holds the string denoting the upstream_model field in the database.
|
||||
FieldUpstreamModel = "upstream_model"
|
||||
// FieldEndpoint holds the string denoting the endpoint field in the database.
|
||||
FieldEndpoint = "endpoint"
|
||||
// FieldPriority holds the string denoting the priority field in the database.
|
||||
FieldPriority = "priority"
|
||||
// FieldEnabled holds the string denoting the enabled field in the database.
|
||||
FieldEnabled = "enabled"
|
||||
// FieldNotes holds the string denoting the notes field in the database.
|
||||
FieldNotes = "notes"
|
||||
// EdgeGroup holds the string denoting the group edge name in mutations.
|
||||
EdgeGroup = "group"
|
||||
// Table holds the table name of the compositemodelroute in the database.
|
||||
Table = "composite_model_routes"
|
||||
// GroupTable is the table that holds the group relation/edge.
|
||||
GroupTable = "composite_model_routes"
|
||||
// GroupInverseTable is the table name for the Group entity.
|
||||
// It exists in this package in order to avoid circular dependency with the "group" package.
|
||||
GroupInverseTable = "groups"
|
||||
// GroupColumn is the table column denoting the group relation/edge.
|
||||
GroupColumn = "group_id"
|
||||
)
|
||||
|
||||
// Columns holds all SQL columns for compositemodelroute fields.
|
||||
var Columns = []string{
|
||||
FieldID,
|
||||
FieldCreatedAt,
|
||||
FieldUpdatedAt,
|
||||
FieldDeletedAt,
|
||||
FieldGroupID,
|
||||
FieldPublicModel,
|
||||
FieldMatchType,
|
||||
FieldTargetPlatform,
|
||||
FieldUpstreamModel,
|
||||
FieldEndpoint,
|
||||
FieldPriority,
|
||||
FieldEnabled,
|
||||
FieldNotes,
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
func ValidColumn(column string) bool {
|
||||
for i := range Columns {
|
||||
if column == Columns[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Note that the variables below are initialized by the runtime
|
||||
// package on the initialization of the application. Therefore,
|
||||
// it should be imported in the main as follows:
|
||||
//
|
||||
// import _ "github.com/Wei-Shaw/sub2api/ent/runtime"
|
||||
var (
|
||||
Hooks [1]ent.Hook
|
||||
Interceptors [1]ent.Interceptor
|
||||
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||
DefaultCreatedAt func() time.Time
|
||||
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
|
||||
DefaultUpdatedAt func() time.Time
|
||||
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
|
||||
UpdateDefaultUpdatedAt func() time.Time
|
||||
// PublicModelValidator is a validator for the "public_model" field. It is called by the builders before save.
|
||||
PublicModelValidator func(string) error
|
||||
// DefaultMatchType holds the default value on creation for the "match_type" field.
|
||||
DefaultMatchType string
|
||||
// MatchTypeValidator is a validator for the "match_type" field. It is called by the builders before save.
|
||||
MatchTypeValidator func(string) error
|
||||
// DefaultTargetPlatform holds the default value on creation for the "target_platform" field.
|
||||
DefaultTargetPlatform string
|
||||
// TargetPlatformValidator is a validator for the "target_platform" field. It is called by the builders before save.
|
||||
TargetPlatformValidator func(string) error
|
||||
// DefaultUpstreamModel holds the default value on creation for the "upstream_model" field.
|
||||
DefaultUpstreamModel string
|
||||
// UpstreamModelValidator is a validator for the "upstream_model" field. It is called by the builders before save.
|
||||
UpstreamModelValidator func(string) error
|
||||
// DefaultEndpoint holds the default value on creation for the "endpoint" field.
|
||||
DefaultEndpoint string
|
||||
// EndpointValidator is a validator for the "endpoint" field. It is called by the builders before save.
|
||||
EndpointValidator func(string) error
|
||||
// DefaultPriority holds the default value on creation for the "priority" field.
|
||||
DefaultPriority int
|
||||
// DefaultEnabled holds the default value on creation for the "enabled" field.
|
||||
DefaultEnabled bool
|
||||
)
|
||||
|
||||
// OrderOption defines the ordering options for the CompositeModelRoute queries.
|
||||
type OrderOption func(*sql.Selector)
|
||||
|
||||
// ByID orders the results by the id field.
|
||||
func ByID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCreatedAt orders the results by the created_at field.
|
||||
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUpdatedAt orders the results by the updated_at field.
|
||||
func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByDeletedAt orders the results by the deleted_at field.
|
||||
func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldDeletedAt, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByGroupID orders the results by the group_id field.
|
||||
func ByGroupID(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldGroupID, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByPublicModel orders the results by the public_model field.
|
||||
func ByPublicModel(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldPublicModel, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByMatchType orders the results by the match_type field.
|
||||
func ByMatchType(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldMatchType, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByTargetPlatform orders the results by the target_platform field.
|
||||
func ByTargetPlatform(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldTargetPlatform, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByUpstreamModel orders the results by the upstream_model field.
|
||||
func ByUpstreamModel(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUpstreamModel, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByEndpoint orders the results by the endpoint field.
|
||||
func ByEndpoint(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldEndpoint, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByPriority orders the results by the priority field.
|
||||
func ByPriority(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldPriority, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByEnabled orders the results by the enabled field.
|
||||
func ByEnabled(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldEnabled, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByNotes orders the results by the notes field.
|
||||
func ByNotes(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldNotes, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByGroupField orders the results by group field.
|
||||
func ByGroupField(field string, opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
sqlgraph.OrderByNeighborTerms(s, newGroupStep(), sql.OrderByField(field, opts...))
|
||||
}
|
||||
}
|
||||
func newGroupStep() *sqlgraph.Step {
|
||||
return sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.To(GroupInverseTable, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, GroupTable, GroupColumn),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
|
||||
package compositemodelroute
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||
)
|
||||
|
||||
// ID filters vertices based on their ID field.
|
||||
func ID(id int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldID, id))
|
||||
}
|
||||
|
||||
// IDEQ applies the EQ predicate on the ID field.
|
||||
func IDEQ(id int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldID, id))
|
||||
}
|
||||
|
||||
// IDNEQ applies the NEQ predicate on the ID field.
|
||||
func IDNEQ(id int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldID, id))
|
||||
}
|
||||
|
||||
// IDIn applies the In predicate on the ID field.
|
||||
func IDIn(ids ...int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldID, ids...))
|
||||
}
|
||||
|
||||
// IDNotIn applies the NotIn predicate on the ID field.
|
||||
func IDNotIn(ids ...int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldID, ids...))
|
||||
}
|
||||
|
||||
// IDGT applies the GT predicate on the ID field.
|
||||
func IDGT(id int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldID, id))
|
||||
}
|
||||
|
||||
// IDGTE applies the GTE predicate on the ID field.
|
||||
func IDGTE(id int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldID, id))
|
||||
}
|
||||
|
||||
// IDLT applies the LT predicate on the ID field.
|
||||
func IDLT(id int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldID, id))
|
||||
}
|
||||
|
||||
// IDLTE applies the LTE predicate on the ID field.
|
||||
func IDLTE(id int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldID, id))
|
||||
}
|
||||
|
||||
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
|
||||
func CreatedAt(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ.
|
||||
func UpdatedAt(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ.
|
||||
func DeletedAt(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// GroupID applies equality check predicate on the "group_id" field. It's identical to GroupIDEQ.
|
||||
func GroupID(v int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldGroupID, v))
|
||||
}
|
||||
|
||||
// PublicModel applies equality check predicate on the "public_model" field. It's identical to PublicModelEQ.
|
||||
func PublicModel(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// MatchType applies equality check predicate on the "match_type" field. It's identical to MatchTypeEQ.
|
||||
func MatchType(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// TargetPlatform applies equality check predicate on the "target_platform" field. It's identical to TargetPlatformEQ.
|
||||
func TargetPlatform(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// UpstreamModel applies equality check predicate on the "upstream_model" field. It's identical to UpstreamModelEQ.
|
||||
func UpstreamModel(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// Endpoint applies equality check predicate on the "endpoint" field. It's identical to EndpointEQ.
|
||||
func Endpoint(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// Priority applies equality check predicate on the "priority" field. It's identical to PriorityEQ.
|
||||
func Priority(v int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldPriority, v))
|
||||
}
|
||||
|
||||
// Enabled applies equality check predicate on the "enabled" field. It's identical to EnabledEQ.
|
||||
func Enabled(v bool) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldEnabled, v))
|
||||
}
|
||||
|
||||
// Notes applies equality check predicate on the "notes" field. It's identical to NotesEQ.
|
||||
func Notes(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldNotes, v))
|
||||
}
|
||||
|
||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||
func CreatedAtEQ(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// CreatedAtNEQ applies the NEQ predicate on the "created_at" field.
|
||||
func CreatedAtNEQ(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// CreatedAtIn applies the In predicate on the "created_at" field.
|
||||
func CreatedAtIn(vs ...time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldCreatedAt, vs...))
|
||||
}
|
||||
|
||||
// CreatedAtNotIn applies the NotIn predicate on the "created_at" field.
|
||||
func CreatedAtNotIn(vs ...time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldCreatedAt, vs...))
|
||||
}
|
||||
|
||||
// CreatedAtGT applies the GT predicate on the "created_at" field.
|
||||
func CreatedAtGT(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// CreatedAtGTE applies the GTE predicate on the "created_at" field.
|
||||
func CreatedAtGTE(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// CreatedAtLT applies the LT predicate on the "created_at" field.
|
||||
func CreatedAtLT(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// CreatedAtLTE applies the LTE predicate on the "created_at" field.
|
||||
func CreatedAtLTE(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldCreatedAt, v))
|
||||
}
|
||||
|
||||
// UpdatedAtEQ applies the EQ predicate on the "updated_at" field.
|
||||
func UpdatedAtEQ(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field.
|
||||
func UpdatedAtNEQ(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// UpdatedAtIn applies the In predicate on the "updated_at" field.
|
||||
func UpdatedAtIn(vs ...time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldUpdatedAt, vs...))
|
||||
}
|
||||
|
||||
// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field.
|
||||
func UpdatedAtNotIn(vs ...time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldUpdatedAt, vs...))
|
||||
}
|
||||
|
||||
// UpdatedAtGT applies the GT predicate on the "updated_at" field.
|
||||
func UpdatedAtGT(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// UpdatedAtGTE applies the GTE predicate on the "updated_at" field.
|
||||
func UpdatedAtGTE(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// UpdatedAtLT applies the LT predicate on the "updated_at" field.
|
||||
func UpdatedAtLT(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// UpdatedAtLTE applies the LTE predicate on the "updated_at" field.
|
||||
func UpdatedAtLTE(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtEQ applies the EQ predicate on the "deleted_at" field.
|
||||
func DeletedAtEQ(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field.
|
||||
func DeletedAtNEQ(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtIn applies the In predicate on the "deleted_at" field.
|
||||
func DeletedAtIn(vs ...time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldDeletedAt, vs...))
|
||||
}
|
||||
|
||||
// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field.
|
||||
func DeletedAtNotIn(vs ...time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldDeletedAt, vs...))
|
||||
}
|
||||
|
||||
// DeletedAtGT applies the GT predicate on the "deleted_at" field.
|
||||
func DeletedAtGT(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtGTE applies the GTE predicate on the "deleted_at" field.
|
||||
func DeletedAtGTE(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtLT applies the LT predicate on the "deleted_at" field.
|
||||
func DeletedAtLT(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtLTE applies the LTE predicate on the "deleted_at" field.
|
||||
func DeletedAtLTE(v time.Time) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field.
|
||||
func DeletedAtIsNil() predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIsNull(FieldDeletedAt))
|
||||
}
|
||||
|
||||
// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field.
|
||||
func DeletedAtNotNil() predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotNull(FieldDeletedAt))
|
||||
}
|
||||
|
||||
// GroupIDEQ applies the EQ predicate on the "group_id" field.
|
||||
func GroupIDEQ(v int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldGroupID, v))
|
||||
}
|
||||
|
||||
// GroupIDNEQ applies the NEQ predicate on the "group_id" field.
|
||||
func GroupIDNEQ(v int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldGroupID, v))
|
||||
}
|
||||
|
||||
// GroupIDIn applies the In predicate on the "group_id" field.
|
||||
func GroupIDIn(vs ...int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldGroupID, vs...))
|
||||
}
|
||||
|
||||
// GroupIDNotIn applies the NotIn predicate on the "group_id" field.
|
||||
func GroupIDNotIn(vs ...int64) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldGroupID, vs...))
|
||||
}
|
||||
|
||||
// PublicModelEQ applies the EQ predicate on the "public_model" field.
|
||||
func PublicModelEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelNEQ applies the NEQ predicate on the "public_model" field.
|
||||
func PublicModelNEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelIn applies the In predicate on the "public_model" field.
|
||||
func PublicModelIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldPublicModel, vs...))
|
||||
}
|
||||
|
||||
// PublicModelNotIn applies the NotIn predicate on the "public_model" field.
|
||||
func PublicModelNotIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldPublicModel, vs...))
|
||||
}
|
||||
|
||||
// PublicModelGT applies the GT predicate on the "public_model" field.
|
||||
func PublicModelGT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelGTE applies the GTE predicate on the "public_model" field.
|
||||
func PublicModelGTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelLT applies the LT predicate on the "public_model" field.
|
||||
func PublicModelLT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelLTE applies the LTE predicate on the "public_model" field.
|
||||
func PublicModelLTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelContains applies the Contains predicate on the "public_model" field.
|
||||
func PublicModelContains(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContains(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelHasPrefix applies the HasPrefix predicate on the "public_model" field.
|
||||
func PublicModelHasPrefix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasPrefix(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelHasSuffix applies the HasSuffix predicate on the "public_model" field.
|
||||
func PublicModelHasSuffix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasSuffix(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelEqualFold applies the EqualFold predicate on the "public_model" field.
|
||||
func PublicModelEqualFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEqualFold(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// PublicModelContainsFold applies the ContainsFold predicate on the "public_model" field.
|
||||
func PublicModelContainsFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContainsFold(FieldPublicModel, v))
|
||||
}
|
||||
|
||||
// MatchTypeEQ applies the EQ predicate on the "match_type" field.
|
||||
func MatchTypeEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeNEQ applies the NEQ predicate on the "match_type" field.
|
||||
func MatchTypeNEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeIn applies the In predicate on the "match_type" field.
|
||||
func MatchTypeIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldMatchType, vs...))
|
||||
}
|
||||
|
||||
// MatchTypeNotIn applies the NotIn predicate on the "match_type" field.
|
||||
func MatchTypeNotIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldMatchType, vs...))
|
||||
}
|
||||
|
||||
// MatchTypeGT applies the GT predicate on the "match_type" field.
|
||||
func MatchTypeGT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeGTE applies the GTE predicate on the "match_type" field.
|
||||
func MatchTypeGTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeLT applies the LT predicate on the "match_type" field.
|
||||
func MatchTypeLT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeLTE applies the LTE predicate on the "match_type" field.
|
||||
func MatchTypeLTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeContains applies the Contains predicate on the "match_type" field.
|
||||
func MatchTypeContains(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContains(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeHasPrefix applies the HasPrefix predicate on the "match_type" field.
|
||||
func MatchTypeHasPrefix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasPrefix(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeHasSuffix applies the HasSuffix predicate on the "match_type" field.
|
||||
func MatchTypeHasSuffix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasSuffix(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeEqualFold applies the EqualFold predicate on the "match_type" field.
|
||||
func MatchTypeEqualFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEqualFold(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// MatchTypeContainsFold applies the ContainsFold predicate on the "match_type" field.
|
||||
func MatchTypeContainsFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContainsFold(FieldMatchType, v))
|
||||
}
|
||||
|
||||
// TargetPlatformEQ applies the EQ predicate on the "target_platform" field.
|
||||
func TargetPlatformEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformNEQ applies the NEQ predicate on the "target_platform" field.
|
||||
func TargetPlatformNEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformIn applies the In predicate on the "target_platform" field.
|
||||
func TargetPlatformIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldTargetPlatform, vs...))
|
||||
}
|
||||
|
||||
// TargetPlatformNotIn applies the NotIn predicate on the "target_platform" field.
|
||||
func TargetPlatformNotIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldTargetPlatform, vs...))
|
||||
}
|
||||
|
||||
// TargetPlatformGT applies the GT predicate on the "target_platform" field.
|
||||
func TargetPlatformGT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformGTE applies the GTE predicate on the "target_platform" field.
|
||||
func TargetPlatformGTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformLT applies the LT predicate on the "target_platform" field.
|
||||
func TargetPlatformLT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformLTE applies the LTE predicate on the "target_platform" field.
|
||||
func TargetPlatformLTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformContains applies the Contains predicate on the "target_platform" field.
|
||||
func TargetPlatformContains(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContains(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformHasPrefix applies the HasPrefix predicate on the "target_platform" field.
|
||||
func TargetPlatformHasPrefix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasPrefix(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformHasSuffix applies the HasSuffix predicate on the "target_platform" field.
|
||||
func TargetPlatformHasSuffix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasSuffix(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformEqualFold applies the EqualFold predicate on the "target_platform" field.
|
||||
func TargetPlatformEqualFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEqualFold(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// TargetPlatformContainsFold applies the ContainsFold predicate on the "target_platform" field.
|
||||
func TargetPlatformContainsFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContainsFold(FieldTargetPlatform, v))
|
||||
}
|
||||
|
||||
// UpstreamModelEQ applies the EQ predicate on the "upstream_model" field.
|
||||
func UpstreamModelEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelNEQ applies the NEQ predicate on the "upstream_model" field.
|
||||
func UpstreamModelNEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelIn applies the In predicate on the "upstream_model" field.
|
||||
func UpstreamModelIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldUpstreamModel, vs...))
|
||||
}
|
||||
|
||||
// UpstreamModelNotIn applies the NotIn predicate on the "upstream_model" field.
|
||||
func UpstreamModelNotIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldUpstreamModel, vs...))
|
||||
}
|
||||
|
||||
// UpstreamModelGT applies the GT predicate on the "upstream_model" field.
|
||||
func UpstreamModelGT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelGTE applies the GTE predicate on the "upstream_model" field.
|
||||
func UpstreamModelGTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelLT applies the LT predicate on the "upstream_model" field.
|
||||
func UpstreamModelLT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelLTE applies the LTE predicate on the "upstream_model" field.
|
||||
func UpstreamModelLTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelContains applies the Contains predicate on the "upstream_model" field.
|
||||
func UpstreamModelContains(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContains(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelHasPrefix applies the HasPrefix predicate on the "upstream_model" field.
|
||||
func UpstreamModelHasPrefix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasPrefix(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelHasSuffix applies the HasSuffix predicate on the "upstream_model" field.
|
||||
func UpstreamModelHasSuffix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasSuffix(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelEqualFold applies the EqualFold predicate on the "upstream_model" field.
|
||||
func UpstreamModelEqualFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEqualFold(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// UpstreamModelContainsFold applies the ContainsFold predicate on the "upstream_model" field.
|
||||
func UpstreamModelContainsFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContainsFold(FieldUpstreamModel, v))
|
||||
}
|
||||
|
||||
// EndpointEQ applies the EQ predicate on the "endpoint" field.
|
||||
func EndpointEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointNEQ applies the NEQ predicate on the "endpoint" field.
|
||||
func EndpointNEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointIn applies the In predicate on the "endpoint" field.
|
||||
func EndpointIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldEndpoint, vs...))
|
||||
}
|
||||
|
||||
// EndpointNotIn applies the NotIn predicate on the "endpoint" field.
|
||||
func EndpointNotIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldEndpoint, vs...))
|
||||
}
|
||||
|
||||
// EndpointGT applies the GT predicate on the "endpoint" field.
|
||||
func EndpointGT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointGTE applies the GTE predicate on the "endpoint" field.
|
||||
func EndpointGTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointLT applies the LT predicate on the "endpoint" field.
|
||||
func EndpointLT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointLTE applies the LTE predicate on the "endpoint" field.
|
||||
func EndpointLTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointContains applies the Contains predicate on the "endpoint" field.
|
||||
func EndpointContains(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContains(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointHasPrefix applies the HasPrefix predicate on the "endpoint" field.
|
||||
func EndpointHasPrefix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasPrefix(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointHasSuffix applies the HasSuffix predicate on the "endpoint" field.
|
||||
func EndpointHasSuffix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasSuffix(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointEqualFold applies the EqualFold predicate on the "endpoint" field.
|
||||
func EndpointEqualFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEqualFold(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// EndpointContainsFold applies the ContainsFold predicate on the "endpoint" field.
|
||||
func EndpointContainsFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContainsFold(FieldEndpoint, v))
|
||||
}
|
||||
|
||||
// PriorityEQ applies the EQ predicate on the "priority" field.
|
||||
func PriorityEQ(v int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldPriority, v))
|
||||
}
|
||||
|
||||
// PriorityNEQ applies the NEQ predicate on the "priority" field.
|
||||
func PriorityNEQ(v int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldPriority, v))
|
||||
}
|
||||
|
||||
// PriorityIn applies the In predicate on the "priority" field.
|
||||
func PriorityIn(vs ...int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldPriority, vs...))
|
||||
}
|
||||
|
||||
// PriorityNotIn applies the NotIn predicate on the "priority" field.
|
||||
func PriorityNotIn(vs ...int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldPriority, vs...))
|
||||
}
|
||||
|
||||
// PriorityGT applies the GT predicate on the "priority" field.
|
||||
func PriorityGT(v int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldPriority, v))
|
||||
}
|
||||
|
||||
// PriorityGTE applies the GTE predicate on the "priority" field.
|
||||
func PriorityGTE(v int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldPriority, v))
|
||||
}
|
||||
|
||||
// PriorityLT applies the LT predicate on the "priority" field.
|
||||
func PriorityLT(v int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldPriority, v))
|
||||
}
|
||||
|
||||
// PriorityLTE applies the LTE predicate on the "priority" field.
|
||||
func PriorityLTE(v int) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldPriority, v))
|
||||
}
|
||||
|
||||
// EnabledEQ applies the EQ predicate on the "enabled" field.
|
||||
func EnabledEQ(v bool) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldEnabled, v))
|
||||
}
|
||||
|
||||
// EnabledNEQ applies the NEQ predicate on the "enabled" field.
|
||||
func EnabledNEQ(v bool) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldEnabled, v))
|
||||
}
|
||||
|
||||
// NotesEQ applies the EQ predicate on the "notes" field.
|
||||
func NotesEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEQ(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesNEQ applies the NEQ predicate on the "notes" field.
|
||||
func NotesNEQ(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNEQ(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesIn applies the In predicate on the "notes" field.
|
||||
func NotesIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIn(FieldNotes, vs...))
|
||||
}
|
||||
|
||||
// NotesNotIn applies the NotIn predicate on the "notes" field.
|
||||
func NotesNotIn(vs ...string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotIn(FieldNotes, vs...))
|
||||
}
|
||||
|
||||
// NotesGT applies the GT predicate on the "notes" field.
|
||||
func NotesGT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGT(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesGTE applies the GTE predicate on the "notes" field.
|
||||
func NotesGTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldGTE(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesLT applies the LT predicate on the "notes" field.
|
||||
func NotesLT(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLT(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesLTE applies the LTE predicate on the "notes" field.
|
||||
func NotesLTE(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldLTE(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesContains applies the Contains predicate on the "notes" field.
|
||||
func NotesContains(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContains(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesHasPrefix applies the HasPrefix predicate on the "notes" field.
|
||||
func NotesHasPrefix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasPrefix(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesHasSuffix applies the HasSuffix predicate on the "notes" field.
|
||||
func NotesHasSuffix(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldHasSuffix(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesIsNil applies the IsNil predicate on the "notes" field.
|
||||
func NotesIsNil() predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldIsNull(FieldNotes))
|
||||
}
|
||||
|
||||
// NotesNotNil applies the NotNil predicate on the "notes" field.
|
||||
func NotesNotNil() predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldNotNull(FieldNotes))
|
||||
}
|
||||
|
||||
// NotesEqualFold applies the EqualFold predicate on the "notes" field.
|
||||
func NotesEqualFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldEqualFold(FieldNotes, v))
|
||||
}
|
||||
|
||||
// NotesContainsFold applies the ContainsFold predicate on the "notes" field.
|
||||
func NotesContainsFold(v string) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.FieldContainsFold(FieldNotes, v))
|
||||
}
|
||||
|
||||
// HasGroup applies the HasEdge predicate on the "group" edge.
|
||||
func HasGroup() predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(func(s *sql.Selector) {
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(Table, FieldID),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, GroupTable, GroupColumn),
|
||||
)
|
||||
sqlgraph.HasNeighbors(s, step)
|
||||
})
|
||||
}
|
||||
|
||||
// HasGroupWith applies the HasEdge predicate on the "group" edge with a given conditions (other predicates).
|
||||
func HasGroupWith(preds ...predicate.Group) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(func(s *sql.Selector) {
|
||||
step := newGroupStep()
|
||||
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
|
||||
for _, p := range preds {
|
||||
p(s)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// And groups predicates with the AND operator between them.
|
||||
func And(predicates ...predicate.CompositeModelRoute) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.AndPredicates(predicates...))
|
||||
}
|
||||
|
||||
// Or groups predicates with the OR operator between them.
|
||||
func Or(predicates ...predicate.CompositeModelRoute) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.OrPredicates(predicates...))
|
||||
}
|
||||
|
||||
// Not applies the not operator on the given predicate.
|
||||
func Not(p predicate.CompositeModelRoute) predicate.CompositeModelRoute {
|
||||
return predicate.CompositeModelRoute(sql.NotPredicates(p))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
|
||||
package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"entgo.io/ent/schema/field"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||
)
|
||||
|
||||
// CompositeModelRouteDelete is the builder for deleting a CompositeModelRoute entity.
|
||||
type CompositeModelRouteDelete struct {
|
||||
config
|
||||
hooks []Hook
|
||||
mutation *CompositeModelRouteMutation
|
||||
}
|
||||
|
||||
// Where appends a list predicates to the CompositeModelRouteDelete builder.
|
||||
func (_d *CompositeModelRouteDelete) Where(ps ...predicate.CompositeModelRoute) *CompositeModelRouteDelete {
|
||||
_d.mutation.Where(ps...)
|
||||
return _d
|
||||
}
|
||||
|
||||
// Exec executes the deletion query and returns how many vertices were deleted.
|
||||
func (_d *CompositeModelRouteDelete) Exec(ctx context.Context) (int, error) {
|
||||
return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
|
||||
}
|
||||
|
||||
// ExecX is like Exec, but panics if an error occurs.
|
||||
func (_d *CompositeModelRouteDelete) ExecX(ctx context.Context) int {
|
||||
n, err := _d.Exec(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (_d *CompositeModelRouteDelete) sqlExec(ctx context.Context) (int, error) {
|
||||
_spec := sqlgraph.NewDeleteSpec(compositemodelroute.Table, sqlgraph.NewFieldSpec(compositemodelroute.FieldID, field.TypeInt64))
|
||||
if ps := _d.mutation.predicates; len(ps) > 0 {
|
||||
_spec.Predicate = func(selector *sql.Selector) {
|
||||
for i := range ps {
|
||||
ps[i](selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
|
||||
if err != nil && sqlgraph.IsConstraintError(err) {
|
||||
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||
}
|
||||
_d.mutation.done = true
|
||||
return affected, err
|
||||
}
|
||||
|
||||
// CompositeModelRouteDeleteOne is the builder for deleting a single CompositeModelRoute entity.
|
||||
type CompositeModelRouteDeleteOne struct {
|
||||
_d *CompositeModelRouteDelete
|
||||
}
|
||||
|
||||
// Where appends a list predicates to the CompositeModelRouteDelete builder.
|
||||
func (_d *CompositeModelRouteDeleteOne) Where(ps ...predicate.CompositeModelRoute) *CompositeModelRouteDeleteOne {
|
||||
_d._d.mutation.Where(ps...)
|
||||
return _d
|
||||
}
|
||||
|
||||
// Exec executes the deletion query.
|
||||
func (_d *CompositeModelRouteDeleteOne) Exec(ctx context.Context) error {
|
||||
n, err := _d._d.Exec(ctx)
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case n == 0:
|
||||
return &NotFoundError{compositemodelroute.Label}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ExecX is like Exec, but panics if an error occurs.
|
||||
func (_d *CompositeModelRouteDeleteOne) ExecX(ctx context.Context) {
|
||||
if err := _d.Exec(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
|
||||
package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"entgo.io/ent/schema/field"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||
)
|
||||
|
||||
// CompositeModelRouteQuery is the builder for querying CompositeModelRoute entities.
|
||||
type CompositeModelRouteQuery struct {
|
||||
config
|
||||
ctx *QueryContext
|
||||
order []compositemodelroute.OrderOption
|
||||
inters []Interceptor
|
||||
predicates []predicate.CompositeModelRoute
|
||||
withGroup *GroupQuery
|
||||
modifiers []func(*sql.Selector)
|
||||
// intermediate query (i.e. traversal path).
|
||||
sql *sql.Selector
|
||||
path func(context.Context) (*sql.Selector, error)
|
||||
}
|
||||
|
||||
// Where adds a new predicate for the CompositeModelRouteQuery builder.
|
||||
func (_q *CompositeModelRouteQuery) Where(ps ...predicate.CompositeModelRoute) *CompositeModelRouteQuery {
|
||||
_q.predicates = append(_q.predicates, ps...)
|
||||
return _q
|
||||
}
|
||||
|
||||
// Limit the number of records to be returned by this query.
|
||||
func (_q *CompositeModelRouteQuery) Limit(limit int) *CompositeModelRouteQuery {
|
||||
_q.ctx.Limit = &limit
|
||||
return _q
|
||||
}
|
||||
|
||||
// Offset to start from.
|
||||
func (_q *CompositeModelRouteQuery) Offset(offset int) *CompositeModelRouteQuery {
|
||||
_q.ctx.Offset = &offset
|
||||
return _q
|
||||
}
|
||||
|
||||
// Unique configures the query builder to filter duplicate records on query.
|
||||
// By default, unique is set to true, and can be disabled using this method.
|
||||
func (_q *CompositeModelRouteQuery) Unique(unique bool) *CompositeModelRouteQuery {
|
||||
_q.ctx.Unique = &unique
|
||||
return _q
|
||||
}
|
||||
|
||||
// Order specifies how the records should be ordered.
|
||||
func (_q *CompositeModelRouteQuery) Order(o ...compositemodelroute.OrderOption) *CompositeModelRouteQuery {
|
||||
_q.order = append(_q.order, o...)
|
||||
return _q
|
||||
}
|
||||
|
||||
// QueryGroup chains the current query on the "group" edge.
|
||||
func (_q *CompositeModelRouteQuery) QueryGroup() *GroupQuery {
|
||||
query := (&GroupClient{config: _q.config}).Query()
|
||||
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
|
||||
if err := _q.prepareQuery(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selector := _q.sqlQuery(ctx)
|
||||
if err := selector.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(compositemodelroute.Table, compositemodelroute.FieldID, selector),
|
||||
sqlgraph.To(group.Table, group.FieldID),
|
||||
sqlgraph.Edge(sqlgraph.M2O, false, compositemodelroute.GroupTable, compositemodelroute.GroupColumn),
|
||||
)
|
||||
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
|
||||
return fromU, nil
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// First returns the first CompositeModelRoute entity from the query.
|
||||
// Returns a *NotFoundError when no CompositeModelRoute was found.
|
||||
func (_q *CompositeModelRouteQuery) First(ctx context.Context) (*CompositeModelRoute, error) {
|
||||
nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return nil, &NotFoundError{compositemodelroute.Label}
|
||||
}
|
||||
return nodes[0], nil
|
||||
}
|
||||
|
||||
// FirstX is like First, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) FirstX(ctx context.Context) *CompositeModelRoute {
|
||||
node, err := _q.First(ctx)
|
||||
if err != nil && !IsNotFound(err) {
|
||||
panic(err)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// FirstID returns the first CompositeModelRoute ID from the query.
|
||||
// Returns a *NotFoundError when no CompositeModelRoute ID was found.
|
||||
func (_q *CompositeModelRouteQuery) FirstID(ctx context.Context) (id int64, err error) {
|
||||
var ids []int64
|
||||
if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
|
||||
return
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
err = &NotFoundError{compositemodelroute.Label}
|
||||
return
|
||||
}
|
||||
return ids[0], nil
|
||||
}
|
||||
|
||||
// FirstIDX is like FirstID, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) FirstIDX(ctx context.Context) int64 {
|
||||
id, err := _q.FirstID(ctx)
|
||||
if err != nil && !IsNotFound(err) {
|
||||
panic(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// Only returns a single CompositeModelRoute entity found by the query, ensuring it only returns one.
|
||||
// Returns a *NotSingularError when more than one CompositeModelRoute entity is found.
|
||||
// Returns a *NotFoundError when no CompositeModelRoute entities are found.
|
||||
func (_q *CompositeModelRouteQuery) Only(ctx context.Context) (*CompositeModelRoute, error) {
|
||||
nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch len(nodes) {
|
||||
case 1:
|
||||
return nodes[0], nil
|
||||
case 0:
|
||||
return nil, &NotFoundError{compositemodelroute.Label}
|
||||
default:
|
||||
return nil, &NotSingularError{compositemodelroute.Label}
|
||||
}
|
||||
}
|
||||
|
||||
// OnlyX is like Only, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) OnlyX(ctx context.Context) *CompositeModelRoute {
|
||||
node, err := _q.Only(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// OnlyID is like Only, but returns the only CompositeModelRoute ID in the query.
|
||||
// Returns a *NotSingularError when more than one CompositeModelRoute ID is found.
|
||||
// Returns a *NotFoundError when no entities are found.
|
||||
func (_q *CompositeModelRouteQuery) OnlyID(ctx context.Context) (id int64, err error) {
|
||||
var ids []int64
|
||||
if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
|
||||
return
|
||||
}
|
||||
switch len(ids) {
|
||||
case 1:
|
||||
id = ids[0]
|
||||
case 0:
|
||||
err = &NotFoundError{compositemodelroute.Label}
|
||||
default:
|
||||
err = &NotSingularError{compositemodelroute.Label}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// OnlyIDX is like OnlyID, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) OnlyIDX(ctx context.Context) int64 {
|
||||
id, err := _q.OnlyID(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// All executes the query and returns a list of CompositeModelRoutes.
|
||||
func (_q *CompositeModelRouteQuery) All(ctx context.Context) ([]*CompositeModelRoute, error) {
|
||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
|
||||
if err := _q.prepareQuery(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qr := querierAll[[]*CompositeModelRoute, *CompositeModelRouteQuery]()
|
||||
return withInterceptors[[]*CompositeModelRoute](ctx, _q, qr, _q.inters)
|
||||
}
|
||||
|
||||
// AllX is like All, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) AllX(ctx context.Context) []*CompositeModelRoute {
|
||||
nodes, err := _q.All(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// IDs executes the query and returns a list of CompositeModelRoute IDs.
|
||||
func (_q *CompositeModelRouteQuery) IDs(ctx context.Context) (ids []int64, err error) {
|
||||
if _q.ctx.Unique == nil && _q.path != nil {
|
||||
_q.Unique(true)
|
||||
}
|
||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
|
||||
if err = _q.Select(compositemodelroute.FieldID).Scan(ctx, &ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// IDsX is like IDs, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) IDsX(ctx context.Context) []int64 {
|
||||
ids, err := _q.IDs(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// Count returns the count of the given query.
|
||||
func (_q *CompositeModelRouteQuery) Count(ctx context.Context) (int, error) {
|
||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
|
||||
if err := _q.prepareQuery(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return withInterceptors[int](ctx, _q, querierCount[*CompositeModelRouteQuery](), _q.inters)
|
||||
}
|
||||
|
||||
// CountX is like Count, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) CountX(ctx context.Context) int {
|
||||
count, err := _q.Count(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Exist returns true if the query has elements in the graph.
|
||||
func (_q *CompositeModelRouteQuery) Exist(ctx context.Context) (bool, error) {
|
||||
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
|
||||
switch _, err := _q.FirstID(ctx); {
|
||||
case IsNotFound(err):
|
||||
return false, nil
|
||||
case err != nil:
|
||||
return false, fmt.Errorf("ent: check existence: %w", err)
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ExistX is like Exist, but panics if an error occurs.
|
||||
func (_q *CompositeModelRouteQuery) ExistX(ctx context.Context) bool {
|
||||
exist, err := _q.Exist(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return exist
|
||||
}
|
||||
|
||||
// Clone returns a duplicate of the CompositeModelRouteQuery builder, including all associated steps. It can be
|
||||
// used to prepare common query builders and use them differently after the clone is made.
|
||||
func (_q *CompositeModelRouteQuery) Clone() *CompositeModelRouteQuery {
|
||||
if _q == nil {
|
||||
return nil
|
||||
}
|
||||
return &CompositeModelRouteQuery{
|
||||
config: _q.config,
|
||||
ctx: _q.ctx.Clone(),
|
||||
order: append([]compositemodelroute.OrderOption{}, _q.order...),
|
||||
inters: append([]Interceptor{}, _q.inters...),
|
||||
predicates: append([]predicate.CompositeModelRoute{}, _q.predicates...),
|
||||
withGroup: _q.withGroup.Clone(),
|
||||
// clone intermediate query.
|
||||
sql: _q.sql.Clone(),
|
||||
path: _q.path,
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroup tells the query-builder to eager-load the nodes that are connected to
|
||||
// the "group" edge. The optional arguments are used to configure the query builder of the edge.
|
||||
func (_q *CompositeModelRouteQuery) WithGroup(opts ...func(*GroupQuery)) *CompositeModelRouteQuery {
|
||||
query := (&GroupClient{config: _q.config}).Query()
|
||||
for _, opt := range opts {
|
||||
opt(query)
|
||||
}
|
||||
_q.withGroup = query
|
||||
return _q
|
||||
}
|
||||
|
||||
// GroupBy is used to group vertices by one or more fields/columns.
|
||||
// It is often used with aggregate functions, like: count, max, mean, min, sum.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var v []struct {
|
||||
// CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
// Count int `json:"count,omitempty"`
|
||||
// }
|
||||
//
|
||||
// client.CompositeModelRoute.Query().
|
||||
// GroupBy(compositemodelroute.FieldCreatedAt).
|
||||
// Aggregate(ent.Count()).
|
||||
// Scan(ctx, &v)
|
||||
func (_q *CompositeModelRouteQuery) GroupBy(field string, fields ...string) *CompositeModelRouteGroupBy {
|
||||
_q.ctx.Fields = append([]string{field}, fields...)
|
||||
grbuild := &CompositeModelRouteGroupBy{build: _q}
|
||||
grbuild.flds = &_q.ctx.Fields
|
||||
grbuild.label = compositemodelroute.Label
|
||||
grbuild.scan = grbuild.Scan
|
||||
return grbuild
|
||||
}
|
||||
|
||||
// Select allows the selection one or more fields/columns for the given query,
|
||||
// instead of selecting all fields in the entity.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var v []struct {
|
||||
// CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
// }
|
||||
//
|
||||
// client.CompositeModelRoute.Query().
|
||||
// Select(compositemodelroute.FieldCreatedAt).
|
||||
// Scan(ctx, &v)
|
||||
func (_q *CompositeModelRouteQuery) Select(fields ...string) *CompositeModelRouteSelect {
|
||||
_q.ctx.Fields = append(_q.ctx.Fields, fields...)
|
||||
sbuild := &CompositeModelRouteSelect{CompositeModelRouteQuery: _q}
|
||||
sbuild.label = compositemodelroute.Label
|
||||
sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
|
||||
return sbuild
|
||||
}
|
||||
|
||||
// Aggregate returns a CompositeModelRouteSelect configured with the given aggregations.
|
||||
func (_q *CompositeModelRouteQuery) Aggregate(fns ...AggregateFunc) *CompositeModelRouteSelect {
|
||||
return _q.Select().Aggregate(fns...)
|
||||
}
|
||||
|
||||
func (_q *CompositeModelRouteQuery) prepareQuery(ctx context.Context) error {
|
||||
for _, inter := range _q.inters {
|
||||
if inter == nil {
|
||||
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
|
||||
}
|
||||
if trv, ok := inter.(Traverser); ok {
|
||||
if err := trv.Traverse(ctx, _q); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range _q.ctx.Fields {
|
||||
if !compositemodelroute.ValidColumn(f) {
|
||||
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
|
||||
}
|
||||
}
|
||||
if _q.path != nil {
|
||||
prev, err := _q.path(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_q.sql = prev
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_q *CompositeModelRouteQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*CompositeModelRoute, error) {
|
||||
var (
|
||||
nodes = []*CompositeModelRoute{}
|
||||
_spec = _q.querySpec()
|
||||
loadedTypes = [1]bool{
|
||||
_q.withGroup != nil,
|
||||
}
|
||||
)
|
||||
_spec.ScanValues = func(columns []string) ([]any, error) {
|
||||
return (*CompositeModelRoute).scanValues(nil, columns)
|
||||
}
|
||||
_spec.Assign = func(columns []string, values []any) error {
|
||||
node := &CompositeModelRoute{config: _q.config}
|
||||
nodes = append(nodes, node)
|
||||
node.Edges.loadedTypes = loadedTypes
|
||||
return node.assignValues(columns, values)
|
||||
}
|
||||
if len(_q.modifiers) > 0 {
|
||||
_spec.Modifiers = _q.modifiers
|
||||
}
|
||||
for i := range hooks {
|
||||
hooks[i](ctx, _spec)
|
||||
}
|
||||
if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return nodes, nil
|
||||
}
|
||||
if query := _q.withGroup; query != nil {
|
||||
if err := _q.loadGroup(ctx, query, nodes, nil,
|
||||
func(n *CompositeModelRoute, e *Group) { n.Edges.Group = e }); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (_q *CompositeModelRouteQuery) loadGroup(ctx context.Context, query *GroupQuery, nodes []*CompositeModelRoute, init func(*CompositeModelRoute), assign func(*CompositeModelRoute, *Group)) error {
|
||||
ids := make([]int64, 0, len(nodes))
|
||||
nodeids := make(map[int64][]*CompositeModelRoute)
|
||||
for i := range nodes {
|
||||
fk := nodes[i].GroupID
|
||||
if _, ok := nodeids[fk]; !ok {
|
||||
ids = append(ids, fk)
|
||||
}
|
||||
nodeids[fk] = append(nodeids[fk], nodes[i])
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
query.Where(group.IDIn(ids...))
|
||||
neighbors, err := query.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range neighbors {
|
||||
nodes, ok := nodeids[n.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf(`unexpected foreign-key "group_id" returned %v`, n.ID)
|
||||
}
|
||||
for i := range nodes {
|
||||
assign(nodes[i], n)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_q *CompositeModelRouteQuery) sqlCount(ctx context.Context) (int, error) {
|
||||
_spec := _q.querySpec()
|
||||
if len(_q.modifiers) > 0 {
|
||||
_spec.Modifiers = _q.modifiers
|
||||
}
|
||||
_spec.Node.Columns = _q.ctx.Fields
|
||||
if len(_q.ctx.Fields) > 0 {
|
||||
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
|
||||
}
|
||||
return sqlgraph.CountNodes(ctx, _q.driver, _spec)
|
||||
}
|
||||
|
||||
func (_q *CompositeModelRouteQuery) querySpec() *sqlgraph.QuerySpec {
|
||||
_spec := sqlgraph.NewQuerySpec(compositemodelroute.Table, compositemodelroute.Columns, sqlgraph.NewFieldSpec(compositemodelroute.FieldID, field.TypeInt64))
|
||||
_spec.From = _q.sql
|
||||
if unique := _q.ctx.Unique; unique != nil {
|
||||
_spec.Unique = *unique
|
||||
} else if _q.path != nil {
|
||||
_spec.Unique = true
|
||||
}
|
||||
if fields := _q.ctx.Fields; len(fields) > 0 {
|
||||
_spec.Node.Columns = make([]string, 0, len(fields))
|
||||
_spec.Node.Columns = append(_spec.Node.Columns, compositemodelroute.FieldID)
|
||||
for i := range fields {
|
||||
if fields[i] != compositemodelroute.FieldID {
|
||||
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
|
||||
}
|
||||
}
|
||||
if _q.withGroup != nil {
|
||||
_spec.Node.AddColumnOnce(compositemodelroute.FieldGroupID)
|
||||
}
|
||||
}
|
||||
if ps := _q.predicates; len(ps) > 0 {
|
||||
_spec.Predicate = func(selector *sql.Selector) {
|
||||
for i := range ps {
|
||||
ps[i](selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
if limit := _q.ctx.Limit; limit != nil {
|
||||
_spec.Limit = *limit
|
||||
}
|
||||
if offset := _q.ctx.Offset; offset != nil {
|
||||
_spec.Offset = *offset
|
||||
}
|
||||
if ps := _q.order; len(ps) > 0 {
|
||||
_spec.Order = func(selector *sql.Selector) {
|
||||
for i := range ps {
|
||||
ps[i](selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
return _spec
|
||||
}
|
||||
|
||||
func (_q *CompositeModelRouteQuery) sqlQuery(ctx context.Context) *sql.Selector {
|
||||
builder := sql.Dialect(_q.driver.Dialect())
|
||||
t1 := builder.Table(compositemodelroute.Table)
|
||||
columns := _q.ctx.Fields
|
||||
if len(columns) == 0 {
|
||||
columns = compositemodelroute.Columns
|
||||
}
|
||||
selector := builder.Select(t1.Columns(columns...)...).From(t1)
|
||||
if _q.sql != nil {
|
||||
selector = _q.sql
|
||||
selector.Select(selector.Columns(columns...)...)
|
||||
}
|
||||
if _q.ctx.Unique != nil && *_q.ctx.Unique {
|
||||
selector.Distinct()
|
||||
}
|
||||
for _, m := range _q.modifiers {
|
||||
m(selector)
|
||||
}
|
||||
for _, p := range _q.predicates {
|
||||
p(selector)
|
||||
}
|
||||
for _, p := range _q.order {
|
||||
p(selector)
|
||||
}
|
||||
if offset := _q.ctx.Offset; offset != nil {
|
||||
// limit is mandatory for offset clause. We start
|
||||
// with default value, and override it below if needed.
|
||||
selector.Offset(*offset).Limit(math.MaxInt32)
|
||||
}
|
||||
if limit := _q.ctx.Limit; limit != nil {
|
||||
selector.Limit(*limit)
|
||||
}
|
||||
return selector
|
||||
}
|
||||
|
||||
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
|
||||
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
|
||||
// either committed or rolled-back.
|
||||
func (_q *CompositeModelRouteQuery) ForUpdate(opts ...sql.LockOption) *CompositeModelRouteQuery {
|
||||
if _q.driver.Dialect() == dialect.Postgres {
|
||||
_q.Unique(false)
|
||||
}
|
||||
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
|
||||
s.ForUpdate(opts...)
|
||||
})
|
||||
return _q
|
||||
}
|
||||
|
||||
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
|
||||
// on any rows that are read. Other sessions can read the rows, but cannot modify them
|
||||
// until your transaction commits.
|
||||
func (_q *CompositeModelRouteQuery) ForShare(opts ...sql.LockOption) *CompositeModelRouteQuery {
|
||||
if _q.driver.Dialect() == dialect.Postgres {
|
||||
_q.Unique(false)
|
||||
}
|
||||
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
|
||||
s.ForShare(opts...)
|
||||
})
|
||||
return _q
|
||||
}
|
||||
|
||||
// CompositeModelRouteGroupBy is the group-by builder for CompositeModelRoute entities.
|
||||
type CompositeModelRouteGroupBy struct {
|
||||
selector
|
||||
build *CompositeModelRouteQuery
|
||||
}
|
||||
|
||||
// Aggregate adds the given aggregation functions to the group-by query.
|
||||
func (_g *CompositeModelRouteGroupBy) Aggregate(fns ...AggregateFunc) *CompositeModelRouteGroupBy {
|
||||
_g.fns = append(_g.fns, fns...)
|
||||
return _g
|
||||
}
|
||||
|
||||
// Scan applies the selector query and scans the result into the given value.
|
||||
func (_g *CompositeModelRouteGroupBy) Scan(ctx context.Context, v any) error {
|
||||
ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
|
||||
if err := _g.build.prepareQuery(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return scanWithInterceptors[*CompositeModelRouteQuery, *CompositeModelRouteGroupBy](ctx, _g.build, _g, _g.build.inters, v)
|
||||
}
|
||||
|
||||
func (_g *CompositeModelRouteGroupBy) sqlScan(ctx context.Context, root *CompositeModelRouteQuery, v any) error {
|
||||
selector := root.sqlQuery(ctx).Select()
|
||||
aggregation := make([]string, 0, len(_g.fns))
|
||||
for _, fn := range _g.fns {
|
||||
aggregation = append(aggregation, fn(selector))
|
||||
}
|
||||
if len(selector.SelectedColumns()) == 0 {
|
||||
columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
|
||||
for _, f := range *_g.flds {
|
||||
columns = append(columns, selector.C(f))
|
||||
}
|
||||
columns = append(columns, aggregation...)
|
||||
selector.Select(columns...)
|
||||
}
|
||||
selector.GroupBy(selector.Columns(*_g.flds...)...)
|
||||
if err := selector.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
rows := &sql.Rows{}
|
||||
query, args := selector.Query()
|
||||
if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
return sql.ScanSlice(rows, v)
|
||||
}
|
||||
|
||||
// CompositeModelRouteSelect is the builder for selecting fields of CompositeModelRoute entities.
|
||||
type CompositeModelRouteSelect struct {
|
||||
*CompositeModelRouteQuery
|
||||
selector
|
||||
}
|
||||
|
||||
// Aggregate adds the given aggregation functions to the selector query.
|
||||
func (_s *CompositeModelRouteSelect) Aggregate(fns ...AggregateFunc) *CompositeModelRouteSelect {
|
||||
_s.fns = append(_s.fns, fns...)
|
||||
return _s
|
||||
}
|
||||
|
||||
// Scan applies the selector query and scans the result into the given value.
|
||||
func (_s *CompositeModelRouteSelect) Scan(ctx context.Context, v any) error {
|
||||
ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
|
||||
if err := _s.prepareQuery(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return scanWithInterceptors[*CompositeModelRouteQuery, *CompositeModelRouteSelect](ctx, _s.CompositeModelRouteQuery, _s, _s.inters, v)
|
||||
}
|
||||
|
||||
func (_s *CompositeModelRouteSelect) sqlScan(ctx context.Context, root *CompositeModelRouteQuery, v any) error {
|
||||
selector := root.sqlQuery(ctx)
|
||||
aggregation := make([]string, 0, len(_s.fns))
|
||||
for _, fn := range _s.fns {
|
||||
aggregation = append(aggregation, fn(selector))
|
||||
}
|
||||
switch n := len(*_s.selector.flds); {
|
||||
case n == 0 && len(aggregation) > 0:
|
||||
selector.Select(aggregation...)
|
||||
case n != 0 && len(aggregation) > 0:
|
||||
selector.AppendSelect(aggregation...)
|
||||
}
|
||||
rows := &sql.Rows{}
|
||||
query, args := selector.Query()
|
||||
if err := _s.driver.Query(ctx, query, args, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
return sql.ScanSlice(rows, v)
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
// Code generated by ent, DO NOT EDIT.
|
||||
|
||||
package ent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"entgo.io/ent/schema/field"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||
)
|
||||
|
||||
// CompositeModelRouteUpdate is the builder for updating CompositeModelRoute entities.
|
||||
type CompositeModelRouteUpdate struct {
|
||||
config
|
||||
hooks []Hook
|
||||
mutation *CompositeModelRouteMutation
|
||||
}
|
||||
|
||||
// Where appends a list predicates to the CompositeModelRouteUpdate builder.
|
||||
func (_u *CompositeModelRouteUpdate) Where(ps ...predicate.CompositeModelRoute) *CompositeModelRouteUpdate {
|
||||
_u.mutation.Where(ps...)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the "updated_at" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetUpdatedAt(v time.Time) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetUpdatedAt(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetDeletedAt(v time.Time) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetDeletedAt(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableDeletedAt(v *time.Time) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetDeletedAt(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (_u *CompositeModelRouteUpdate) ClearDeletedAt() *CompositeModelRouteUpdate {
|
||||
_u.mutation.ClearDeletedAt()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetGroupID sets the "group_id" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetGroupID(v int64) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetGroupID(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableGroupID sets the "group_id" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableGroupID(v *int64) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetGroupID(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetPublicModel sets the "public_model" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetPublicModel(v string) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetPublicModel(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillablePublicModel sets the "public_model" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillablePublicModel(v *string) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetPublicModel(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMatchType sets the "match_type" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetMatchType(v string) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetMatchType(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMatchType sets the "match_type" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableMatchType(v *string) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetMatchType(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetTargetPlatform sets the "target_platform" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetTargetPlatform(v string) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetTargetPlatform(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableTargetPlatform sets the "target_platform" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableTargetPlatform(v *string) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetTargetPlatform(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUpstreamModel sets the "upstream_model" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetUpstreamModel(v string) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetUpstreamModel(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUpstreamModel sets the "upstream_model" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableUpstreamModel(v *string) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetUpstreamModel(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetEndpoint sets the "endpoint" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetEndpoint(v string) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetEndpoint(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableEndpoint sets the "endpoint" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableEndpoint(v *string) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetEndpoint(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetPriority sets the "priority" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetPriority(v int) *CompositeModelRouteUpdate {
|
||||
_u.mutation.ResetPriority()
|
||||
_u.mutation.SetPriority(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillablePriority sets the "priority" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillablePriority(v *int) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetPriority(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddPriority adds value to the "priority" field.
|
||||
func (_u *CompositeModelRouteUpdate) AddPriority(v int) *CompositeModelRouteUpdate {
|
||||
_u.mutation.AddPriority(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetEnabled sets the "enabled" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetEnabled(v bool) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetEnabled(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableEnabled(v *bool) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetEnabled(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNotes sets the "notes" field.
|
||||
func (_u *CompositeModelRouteUpdate) SetNotes(v string) *CompositeModelRouteUpdate {
|
||||
_u.mutation.SetNotes(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableNotes sets the "notes" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdate) SetNillableNotes(v *string) *CompositeModelRouteUpdate {
|
||||
if v != nil {
|
||||
_u.SetNotes(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearNotes clears the value of the "notes" field.
|
||||
func (_u *CompositeModelRouteUpdate) ClearNotes() *CompositeModelRouteUpdate {
|
||||
_u.mutation.ClearNotes()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetGroup sets the "group" edge to the Group entity.
|
||||
func (_u *CompositeModelRouteUpdate) SetGroup(v *Group) *CompositeModelRouteUpdate {
|
||||
return _u.SetGroupID(v.ID)
|
||||
}
|
||||
|
||||
// Mutation returns the CompositeModelRouteMutation object of the builder.
|
||||
func (_u *CompositeModelRouteUpdate) Mutation() *CompositeModelRouteMutation {
|
||||
return _u.mutation
|
||||
}
|
||||
|
||||
// ClearGroup clears the "group" edge to the Group entity.
|
||||
func (_u *CompositeModelRouteUpdate) ClearGroup() *CompositeModelRouteUpdate {
|
||||
_u.mutation.ClearGroup()
|
||||
return _u
|
||||
}
|
||||
|
||||
// Save executes the query and returns the number of nodes affected by the update operation.
|
||||
func (_u *CompositeModelRouteUpdate) Save(ctx context.Context) (int, error) {
|
||||
if err := _u.defaults(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
|
||||
}
|
||||
|
||||
// SaveX is like Save, but panics if an error occurs.
|
||||
func (_u *CompositeModelRouteUpdate) SaveX(ctx context.Context) int {
|
||||
affected, err := _u.Save(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return affected
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (_u *CompositeModelRouteUpdate) Exec(ctx context.Context) error {
|
||||
_, err := _u.Save(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// ExecX is like Exec, but panics if an error occurs.
|
||||
func (_u *CompositeModelRouteUpdate) ExecX(ctx context.Context) {
|
||||
if err := _u.Exec(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// defaults sets the default values of the builder before save.
|
||||
func (_u *CompositeModelRouteUpdate) defaults() error {
|
||||
if _, ok := _u.mutation.UpdatedAt(); !ok {
|
||||
if compositemodelroute.UpdateDefaultUpdatedAt == nil {
|
||||
return fmt.Errorf("ent: uninitialized compositemodelroute.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)")
|
||||
}
|
||||
v := compositemodelroute.UpdateDefaultUpdatedAt()
|
||||
_u.mutation.SetUpdatedAt(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (_u *CompositeModelRouteUpdate) check() error {
|
||||
if v, ok := _u.mutation.PublicModel(); ok {
|
||||
if err := compositemodelroute.PublicModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "public_model", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.public_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.MatchType(); ok {
|
||||
if err := compositemodelroute.MatchTypeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "match_type", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.match_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.TargetPlatform(); ok {
|
||||
if err := compositemodelroute.TargetPlatformValidator(v); err != nil {
|
||||
return &ValidationError{Name: "target_platform", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.target_platform": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.UpstreamModel(); ok {
|
||||
if err := compositemodelroute.UpstreamModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "upstream_model", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.upstream_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.Endpoint(); ok {
|
||||
if err := compositemodelroute.EndpointValidator(v); err != nil {
|
||||
return &ValidationError{Name: "endpoint", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.endpoint": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.GroupCleared() && len(_u.mutation.GroupIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "CompositeModelRoute.group"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_u *CompositeModelRouteUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if err := _u.check(); err != nil {
|
||||
return _node, err
|
||||
}
|
||||
_spec := sqlgraph.NewUpdateSpec(compositemodelroute.Table, compositemodelroute.Columns, sqlgraph.NewFieldSpec(compositemodelroute.FieldID, field.TypeInt64))
|
||||
if ps := _u.mutation.predicates; len(ps) > 0 {
|
||||
_spec.Predicate = func(selector *sql.Selector) {
|
||||
for i := range ps {
|
||||
ps[i](selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldUpdatedAt, field.TypeTime, value)
|
||||
}
|
||||
if value, ok := _u.mutation.DeletedAt(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldDeletedAt, field.TypeTime, value)
|
||||
}
|
||||
if _u.mutation.DeletedAtCleared() {
|
||||
_spec.ClearField(compositemodelroute.FieldDeletedAt, field.TypeTime)
|
||||
}
|
||||
if value, ok := _u.mutation.PublicModel(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldPublicModel, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MatchType(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldMatchType, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.TargetPlatform(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldTargetPlatform, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.UpstreamModel(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldUpstreamModel, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Endpoint(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldEndpoint, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Priority(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldPriority, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedPriority(); ok {
|
||||
_spec.AddField(compositemodelroute.FieldPriority, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Enabled(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldEnabled, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Notes(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldNotes, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.NotesCleared() {
|
||||
_spec.ClearField(compositemodelroute.FieldNotes, field.TypeString)
|
||||
}
|
||||
if _u.mutation.GroupCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: compositemodelroute.GroupTable,
|
||||
Columns: []string{compositemodelroute.GroupColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
|
||||
}
|
||||
if nodes := _u.mutation.GroupIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: compositemodelroute.GroupTable,
|
||||
Columns: []string{compositemodelroute.GroupColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_spec.Edges.Add = append(_spec.Edges.Add, edge)
|
||||
}
|
||||
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
|
||||
if _, ok := err.(*sqlgraph.NotFoundError); ok {
|
||||
err = &NotFoundError{compositemodelroute.Label}
|
||||
} else if sqlgraph.IsConstraintError(err) {
|
||||
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
_u.mutation.done = true
|
||||
return _node, nil
|
||||
}
|
||||
|
||||
// CompositeModelRouteUpdateOne is the builder for updating a single CompositeModelRoute entity.
|
||||
type CompositeModelRouteUpdateOne struct {
|
||||
config
|
||||
fields []string
|
||||
hooks []Hook
|
||||
mutation *CompositeModelRouteMutation
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the "updated_at" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetUpdatedAt(v time.Time) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetUpdatedAt(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetDeletedAt(v time.Time) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetDeletedAt(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableDeletedAt(v *time.Time) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetDeletedAt(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) ClearDeletedAt() *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.ClearDeletedAt()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetGroupID sets the "group_id" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetGroupID(v int64) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetGroupID(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableGroupID sets the "group_id" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableGroupID(v *int64) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetGroupID(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetPublicModel sets the "public_model" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetPublicModel(v string) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetPublicModel(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillablePublicModel sets the "public_model" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillablePublicModel(v *string) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetPublicModel(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMatchType sets the "match_type" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetMatchType(v string) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetMatchType(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMatchType sets the "match_type" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableMatchType(v *string) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetMatchType(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetTargetPlatform sets the "target_platform" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetTargetPlatform(v string) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetTargetPlatform(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableTargetPlatform sets the "target_platform" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableTargetPlatform(v *string) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetTargetPlatform(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUpstreamModel sets the "upstream_model" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetUpstreamModel(v string) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetUpstreamModel(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableUpstreamModel sets the "upstream_model" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableUpstreamModel(v *string) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetUpstreamModel(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetEndpoint sets the "endpoint" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetEndpoint(v string) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetEndpoint(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableEndpoint sets the "endpoint" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableEndpoint(v *string) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetEndpoint(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetPriority sets the "priority" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetPriority(v int) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.ResetPriority()
|
||||
_u.mutation.SetPriority(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillablePriority sets the "priority" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillablePriority(v *int) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetPriority(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddPriority adds value to the "priority" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) AddPriority(v int) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.AddPriority(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetEnabled sets the "enabled" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetEnabled(v bool) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetEnabled(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableEnabled sets the "enabled" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableEnabled(v *bool) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetEnabled(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNotes sets the "notes" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNotes(v string) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.SetNotes(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableNotes sets the "notes" field if the given value is not nil.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetNillableNotes(v *string) *CompositeModelRouteUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetNotes(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearNotes clears the value of the "notes" field.
|
||||
func (_u *CompositeModelRouteUpdateOne) ClearNotes() *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.ClearNotes()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetGroup sets the "group" edge to the Group entity.
|
||||
func (_u *CompositeModelRouteUpdateOne) SetGroup(v *Group) *CompositeModelRouteUpdateOne {
|
||||
return _u.SetGroupID(v.ID)
|
||||
}
|
||||
|
||||
// Mutation returns the CompositeModelRouteMutation object of the builder.
|
||||
func (_u *CompositeModelRouteUpdateOne) Mutation() *CompositeModelRouteMutation {
|
||||
return _u.mutation
|
||||
}
|
||||
|
||||
// ClearGroup clears the "group" edge to the Group entity.
|
||||
func (_u *CompositeModelRouteUpdateOne) ClearGroup() *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.ClearGroup()
|
||||
return _u
|
||||
}
|
||||
|
||||
// Where appends a list predicates to the CompositeModelRouteUpdate builder.
|
||||
func (_u *CompositeModelRouteUpdateOne) Where(ps ...predicate.CompositeModelRoute) *CompositeModelRouteUpdateOne {
|
||||
_u.mutation.Where(ps...)
|
||||
return _u
|
||||
}
|
||||
|
||||
// Select allows selecting one or more fields (columns) of the returned entity.
|
||||
// The default is selecting all fields defined in the entity schema.
|
||||
func (_u *CompositeModelRouteUpdateOne) Select(field string, fields ...string) *CompositeModelRouteUpdateOne {
|
||||
_u.fields = append([]string{field}, fields...)
|
||||
return _u
|
||||
}
|
||||
|
||||
// Save executes the query and returns the updated CompositeModelRoute entity.
|
||||
func (_u *CompositeModelRouteUpdateOne) Save(ctx context.Context) (*CompositeModelRoute, error) {
|
||||
if err := _u.defaults(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
|
||||
}
|
||||
|
||||
// SaveX is like Save, but panics if an error occurs.
|
||||
func (_u *CompositeModelRouteUpdateOne) SaveX(ctx context.Context) *CompositeModelRoute {
|
||||
node, err := _u.Save(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// Exec executes the query on the entity.
|
||||
func (_u *CompositeModelRouteUpdateOne) Exec(ctx context.Context) error {
|
||||
_, err := _u.Save(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// ExecX is like Exec, but panics if an error occurs.
|
||||
func (_u *CompositeModelRouteUpdateOne) ExecX(ctx context.Context) {
|
||||
if err := _u.Exec(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// defaults sets the default values of the builder before save.
|
||||
func (_u *CompositeModelRouteUpdateOne) defaults() error {
|
||||
if _, ok := _u.mutation.UpdatedAt(); !ok {
|
||||
if compositemodelroute.UpdateDefaultUpdatedAt == nil {
|
||||
return fmt.Errorf("ent: uninitialized compositemodelroute.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)")
|
||||
}
|
||||
v := compositemodelroute.UpdateDefaultUpdatedAt()
|
||||
_u.mutation.SetUpdatedAt(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (_u *CompositeModelRouteUpdateOne) check() error {
|
||||
if v, ok := _u.mutation.PublicModel(); ok {
|
||||
if err := compositemodelroute.PublicModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "public_model", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.public_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.MatchType(); ok {
|
||||
if err := compositemodelroute.MatchTypeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "match_type", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.match_type": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.TargetPlatform(); ok {
|
||||
if err := compositemodelroute.TargetPlatformValidator(v); err != nil {
|
||||
return &ValidationError{Name: "target_platform", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.target_platform": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.UpstreamModel(); ok {
|
||||
if err := compositemodelroute.UpstreamModelValidator(v); err != nil {
|
||||
return &ValidationError{Name: "upstream_model", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.upstream_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.Endpoint(); ok {
|
||||
if err := compositemodelroute.EndpointValidator(v); err != nil {
|
||||
return &ValidationError{Name: "endpoint", err: fmt.Errorf(`ent: validator failed for field "CompositeModelRoute.endpoint": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.GroupCleared() && len(_u.mutation.GroupIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "CompositeModelRoute.group"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_u *CompositeModelRouteUpdateOne) sqlSave(ctx context.Context) (_node *CompositeModelRoute, err error) {
|
||||
if err := _u.check(); err != nil {
|
||||
return _node, err
|
||||
}
|
||||
_spec := sqlgraph.NewUpdateSpec(compositemodelroute.Table, compositemodelroute.Columns, sqlgraph.NewFieldSpec(compositemodelroute.FieldID, field.TypeInt64))
|
||||
id, ok := _u.mutation.ID()
|
||||
if !ok {
|
||||
return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "CompositeModelRoute.id" for update`)}
|
||||
}
|
||||
_spec.Node.ID.Value = id
|
||||
if fields := _u.fields; len(fields) > 0 {
|
||||
_spec.Node.Columns = make([]string, 0, len(fields))
|
||||
_spec.Node.Columns = append(_spec.Node.Columns, compositemodelroute.FieldID)
|
||||
for _, f := range fields {
|
||||
if !compositemodelroute.ValidColumn(f) {
|
||||
return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
|
||||
}
|
||||
if f != compositemodelroute.FieldID {
|
||||
_spec.Node.Columns = append(_spec.Node.Columns, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
if ps := _u.mutation.predicates; len(ps) > 0 {
|
||||
_spec.Predicate = func(selector *sql.Selector) {
|
||||
for i := range ps {
|
||||
ps[i](selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldUpdatedAt, field.TypeTime, value)
|
||||
}
|
||||
if value, ok := _u.mutation.DeletedAt(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldDeletedAt, field.TypeTime, value)
|
||||
}
|
||||
if _u.mutation.DeletedAtCleared() {
|
||||
_spec.ClearField(compositemodelroute.FieldDeletedAt, field.TypeTime)
|
||||
}
|
||||
if value, ok := _u.mutation.PublicModel(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldPublicModel, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MatchType(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldMatchType, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.TargetPlatform(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldTargetPlatform, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.UpstreamModel(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldUpstreamModel, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Endpoint(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldEndpoint, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Priority(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldPriority, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedPriority(); ok {
|
||||
_spec.AddField(compositemodelroute.FieldPriority, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Enabled(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldEnabled, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.Notes(); ok {
|
||||
_spec.SetField(compositemodelroute.FieldNotes, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.NotesCleared() {
|
||||
_spec.ClearField(compositemodelroute.FieldNotes, field.TypeString)
|
||||
}
|
||||
if _u.mutation.GroupCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: compositemodelroute.GroupTable,
|
||||
Columns: []string{compositemodelroute.GroupColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
|
||||
}
|
||||
if nodes := _u.mutation.GroupIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
Inverse: false,
|
||||
Table: compositemodelroute.GroupTable,
|
||||
Columns: []string{compositemodelroute.GroupColumn},
|
||||
Bidi: false,
|
||||
Target: &sqlgraph.EdgeTarget{
|
||||
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt64),
|
||||
},
|
||||
}
|
||||
for _, k := range nodes {
|
||||
edge.Target.Nodes = append(edge.Target.Nodes, k)
|
||||
}
|
||||
_spec.Edges.Add = append(_spec.Edges.Add, edge)
|
||||
}
|
||||
_node = &CompositeModelRoute{config: _u.config}
|
||||
_spec.Assign = _node.assignValues
|
||||
_spec.ScanValues = _node.scanValues
|
||||
if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil {
|
||||
if _, ok := err.(*sqlgraph.NotFoundError); ok {
|
||||
err = &NotFoundError{compositemodelroute.Label}
|
||||
} else if sqlgraph.IsConstraintError(err) {
|
||||
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
_u.mutation.done = true
|
||||
return _node, nil
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
|
||||
@@ -124,6 +125,7 @@ func checkColumn(t, c string) error {
|
||||
channelmonitordailyrollup.Table: channelmonitordailyrollup.ValidColumn,
|
||||
channelmonitorhistory.Table: channelmonitorhistory.ValidColumn,
|
||||
channelmonitorrequesttemplate.Table: channelmonitorrequesttemplate.ValidColumn,
|
||||
compositemodelroute.Table: compositemodelroute.ValidColumn,
|
||||
errorpassthroughrule.Table: errorpassthroughrule.ValidColumn,
|
||||
group.Table: group.ValidColumn,
|
||||
idempotencyrecord.Table: idempotencyrecord.ValidColumn,
|
||||
|
||||
@@ -105,6 +105,8 @@ type Group struct {
|
||||
SortOrder int `json:"sort_order,omitempty"`
|
||||
// 是否允许 /v1/messages 调度到此 OpenAI 分组
|
||||
AllowMessagesDispatch bool `json:"allow_messages_dispatch,omitempty"`
|
||||
// 是否允许此 OpenAI 分组访问 Live 接口
|
||||
AllowLive bool `json:"allow_live,omitempty"`
|
||||
// 仅允许非 apikey 类型账号关联到此分组
|
||||
RequireOauthOnly bool `json:"require_oauth_only,omitempty"`
|
||||
// 调度时仅允许 privacy 已成功设置的账号
|
||||
@@ -117,6 +119,16 @@ type Group struct {
|
||||
ModelsListConfig domain.GroupModelsListConfig `json:"models_list_config,omitempty"`
|
||||
// 分组 RPM 上限,0 表示不限制;设置后接管该分组用户的限流
|
||||
RpmLimit int `json:"rpm_limit,omitempty"`
|
||||
// OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max
|
||||
MaxReasoningEffort string `json:"max_reasoning_effort,omitempty"`
|
||||
// OpenAI reasoning effort 自定义精确映射;先映射再应用上限
|
||||
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"`
|
||||
@@ -223,15 +235,15 @@ 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:
|
||||
case 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.FieldRequireOauthOnly, group.FieldRequirePrivacySet:
|
||||
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, 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.FieldProfitMinMargin, group.FieldProfitSafetyBuffer:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldRpmLimit:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel:
|
||||
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel, group.FieldMaxReasoningEffort:
|
||||
values[i] = new(sql.NullString)
|
||||
case group.FieldCreatedAt, group.FieldUpdatedAt, group.FieldDeletedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
@@ -533,6 +545,12 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.AllowMessagesDispatch = value.Bool
|
||||
}
|
||||
case group.FieldAllowLive:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field allow_live", values[i])
|
||||
} else if value.Valid {
|
||||
_m.AllowLive = value.Bool
|
||||
}
|
||||
case group.FieldRequireOauthOnly:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field require_oauth_only", values[i])
|
||||
@@ -573,6 +591,38 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.RpmLimit = int(value.Int64)
|
||||
}
|
||||
case group.FieldMaxReasoningEffort:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field max_reasoning_effort", values[i])
|
||||
} else if value.Valid {
|
||||
_m.MaxReasoningEffort = value.String
|
||||
}
|
||||
case group.FieldReasoningEffortMappings:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field reasoning_effort_mappings", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &_m.ReasoningEffortMappings); err != nil {
|
||||
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])
|
||||
}
|
||||
@@ -808,6 +858,9 @@ func (_m *Group) String() string {
|
||||
builder.WriteString("allow_messages_dispatch=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.AllowMessagesDispatch))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("allow_live=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.AllowLive))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("require_oauth_only=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.RequireOauthOnly))
|
||||
builder.WriteString(", ")
|
||||
@@ -825,6 +878,21 @@ func (_m *Group) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("rpm_limit=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.RpmLimit))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("max_reasoning_effort=")
|
||||
builder.WriteString(_m.MaxReasoningEffort)
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ const (
|
||||
FieldSortOrder = "sort_order"
|
||||
// FieldAllowMessagesDispatch holds the string denoting the allow_messages_dispatch field in the database.
|
||||
FieldAllowMessagesDispatch = "allow_messages_dispatch"
|
||||
// FieldAllowLive holds the string denoting the allow_live field in the database.
|
||||
FieldAllowLive = "allow_live"
|
||||
// FieldRequireOauthOnly holds the string denoting the require_oauth_only field in the database.
|
||||
FieldRequireOauthOnly = "require_oauth_only"
|
||||
// FieldRequirePrivacySet holds the string denoting the require_privacy_set field in the database.
|
||||
@@ -114,6 +116,16 @@ const (
|
||||
FieldModelsListConfig = "models_list_config"
|
||||
// FieldRpmLimit holds the string denoting the rpm_limit field in the database.
|
||||
FieldRpmLimit = "rpm_limit"
|
||||
// FieldMaxReasoningEffort holds the string denoting the max_reasoning_effort field in the database.
|
||||
FieldMaxReasoningEffort = "max_reasoning_effort"
|
||||
// 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.
|
||||
@@ -232,12 +244,18 @@ var Columns = []string{
|
||||
FieldSupportedModelScopes,
|
||||
FieldSortOrder,
|
||||
FieldAllowMessagesDispatch,
|
||||
FieldAllowLive,
|
||||
FieldRequireOauthOnly,
|
||||
FieldRequirePrivacySet,
|
||||
FieldDefaultMappedModel,
|
||||
FieldMessagesDispatchModelConfig,
|
||||
FieldModelsListConfig,
|
||||
FieldRpmLimit,
|
||||
FieldMaxReasoningEffort,
|
||||
FieldReasoningEffortMappings,
|
||||
FieldProfitControlEnabled,
|
||||
FieldProfitMinMargin,
|
||||
FieldProfitSafetyBuffer,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -335,6 +353,8 @@ var (
|
||||
DefaultSortOrder int
|
||||
// DefaultAllowMessagesDispatch holds the default value on creation for the "allow_messages_dispatch" field.
|
||||
DefaultAllowMessagesDispatch bool
|
||||
// DefaultAllowLive holds the default value on creation for the "allow_live" field.
|
||||
DefaultAllowLive bool
|
||||
// DefaultRequireOauthOnly holds the default value on creation for the "require_oauth_only" field.
|
||||
DefaultRequireOauthOnly bool
|
||||
// DefaultRequirePrivacySet holds the default value on creation for the "require_privacy_set" field.
|
||||
@@ -349,6 +369,18 @@ var (
|
||||
DefaultModelsListConfig domain.GroupModelsListConfig
|
||||
// DefaultRpmLimit holds the default value on creation for the "rpm_limit" field.
|
||||
DefaultRpmLimit int
|
||||
// DefaultMaxReasoningEffort holds the default value on creation for the "max_reasoning_effort" field.
|
||||
DefaultMaxReasoningEffort string
|
||||
// MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
|
||||
MaxReasoningEffortValidator func(string) error
|
||||
// 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.
|
||||
@@ -564,6 +596,11 @@ func ByAllowMessagesDispatch(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAllowMessagesDispatch, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAllowLive orders the results by the allow_live field.
|
||||
func ByAllowLive(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAllowLive, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByRequireOauthOnly orders the results by the require_oauth_only field.
|
||||
func ByRequireOauthOnly(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldRequireOauthOnly, opts...).ToFunc()
|
||||
@@ -584,6 +621,26 @@ func ByRpmLimit(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldRpmLimit, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByMaxReasoningEffort orders the results by the max_reasoning_effort field.
|
||||
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) {
|
||||
|
||||
@@ -260,6 +260,11 @@ func AllowMessagesDispatch(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAllowMessagesDispatch, v))
|
||||
}
|
||||
|
||||
// AllowLive applies equality check predicate on the "allow_live" field. It's identical to AllowLiveEQ.
|
||||
func AllowLive(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAllowLive, v))
|
||||
}
|
||||
|
||||
// RequireOauthOnly applies equality check predicate on the "require_oauth_only" field. It's identical to RequireOauthOnlyEQ.
|
||||
func RequireOauthOnly(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
|
||||
@@ -280,6 +285,26 @@ func RpmLimit(v int) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldRpmLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffort applies equality check predicate on the "max_reasoning_effort" field. It's identical to MaxReasoningEffortEQ.
|
||||
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))
|
||||
@@ -1980,6 +2005,16 @@ func AllowMessagesDispatchNEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldAllowMessagesDispatch, v))
|
||||
}
|
||||
|
||||
// AllowLiveEQ applies the EQ predicate on the "allow_live" field.
|
||||
func AllowLiveEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldAllowLive, v))
|
||||
}
|
||||
|
||||
// AllowLiveNEQ applies the NEQ predicate on the "allow_live" field.
|
||||
func AllowLiveNEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldAllowLive, v))
|
||||
}
|
||||
|
||||
// RequireOauthOnlyEQ applies the EQ predicate on the "require_oauth_only" field.
|
||||
func RequireOauthOnlyEQ(v bool) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
|
||||
@@ -2105,6 +2140,161 @@ func RpmLimitLTE(v int) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldRpmLimit, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortEQ applies the EQ predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortEQ(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortNEQ applies the NEQ predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortNEQ(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortIn applies the In predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortIn(vs ...string) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldMaxReasoningEffort, vs...))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortNotIn applies the NotIn predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortNotIn(vs ...string) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldMaxReasoningEffort, vs...))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortGT applies the GT predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortGT(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortGTE applies the GTE predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortGTE(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortLT applies the LT predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortLT(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortLTE applies the LTE predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortLTE(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortContains applies the Contains predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortContains(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldContains(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortHasPrefix applies the HasPrefix predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortHasPrefix(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldHasPrefix(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortHasSuffix applies the HasSuffix predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortHasSuffix(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldHasSuffix(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortEqualFold applies the EqualFold predicate on the "max_reasoning_effort" field.
|
||||
func MaxReasoningEffortEqualFold(v string) predicate.Group {
|
||||
return predicate.Group(sql.FieldEqualFold(FieldMaxReasoningEffort, v))
|
||||
}
|
||||
|
||||
// MaxReasoningEffortContainsFold applies the ContainsFold predicate on the "max_reasoning_effort" field.
|
||||
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) {
|
||||
|
||||
@@ -607,6 +607,20 @@ func (_c *GroupCreate) SetNillableAllowMessagesDispatch(v *bool) *GroupCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAllowLive sets the "allow_live" field.
|
||||
func (_c *GroupCreate) SetAllowLive(v bool) *GroupCreate {
|
||||
_c.mutation.SetAllowLive(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableAllowLive sets the "allow_live" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableAllowLive(v *bool) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetAllowLive(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (_c *GroupCreate) SetRequireOauthOnly(v bool) *GroupCreate {
|
||||
_c.mutation.SetRequireOauthOnly(v)
|
||||
@@ -691,6 +705,68 @@ func (_c *GroupCreate) SetNillableRpmLimit(v *int) *GroupCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffort sets the "max_reasoning_effort" field.
|
||||
func (_c *GroupCreate) SetMaxReasoningEffort(v string) *GroupCreate {
|
||||
_c.mutation.SetMaxReasoningEffort(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableMaxReasoningEffort sets the "max_reasoning_effort" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableMaxReasoningEffort(v *string) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetMaxReasoningEffort(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (_c *GroupCreate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupCreate {
|
||||
_c.mutation.SetReasoningEffortMappings(v)
|
||||
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...)
|
||||
@@ -928,6 +1004,10 @@ func (_c *GroupCreate) defaults() error {
|
||||
v := group.DefaultAllowMessagesDispatch
|
||||
_c.mutation.SetAllowMessagesDispatch(v)
|
||||
}
|
||||
if _, ok := _c.mutation.AllowLive(); !ok {
|
||||
v := group.DefaultAllowLive
|
||||
_c.mutation.SetAllowLive(v)
|
||||
}
|
||||
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
|
||||
v := group.DefaultRequireOauthOnly
|
||||
_c.mutation.SetRequireOauthOnly(v)
|
||||
@@ -952,6 +1032,26 @@ func (_c *GroupCreate) defaults() error {
|
||||
v := group.DefaultRpmLimit
|
||||
_c.mutation.SetRpmLimit(v)
|
||||
}
|
||||
if _, ok := _c.mutation.MaxReasoningEffort(); !ok {
|
||||
v := group.DefaultMaxReasoningEffort
|
||||
_c.mutation.SetMaxReasoningEffort(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1073,6 +1173,9 @@ func (_c *GroupCreate) check() error {
|
||||
if _, ok := _c.mutation.AllowMessagesDispatch(); !ok {
|
||||
return &ValidationError{Name: "allow_messages_dispatch", err: errors.New(`ent: missing required field "Group.allow_messages_dispatch"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.AllowLive(); !ok {
|
||||
return &ValidationError{Name: "allow_live", err: errors.New(`ent: missing required field "Group.allow_live"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
|
||||
return &ValidationError{Name: "require_oauth_only", err: errors.New(`ent: missing required field "Group.require_oauth_only"`)}
|
||||
}
|
||||
@@ -1096,6 +1199,26 @@ func (_c *GroupCreate) check() error {
|
||||
if _, ok := _c.mutation.RpmLimit(); !ok {
|
||||
return &ValidationError{Name: "rpm_limit", err: errors.New(`ent: missing required field "Group.rpm_limit"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.MaxReasoningEffort(); !ok {
|
||||
return &ValidationError{Name: "max_reasoning_effort", err: errors.New(`ent: missing required field "Group.max_reasoning_effort"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.MaxReasoningEffort(); ok {
|
||||
if err := group.MaxReasoningEffortValidator(v); err != nil {
|
||||
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
|
||||
return &ValidationError{Name: "reasoning_effort_mappings", err: errors.New(`ent: missing required field "Group.reasoning_effort_mappings"`)}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1295,6 +1418,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldAllowMessagesDispatch, field.TypeBool, value)
|
||||
_node.AllowMessagesDispatch = value
|
||||
}
|
||||
if value, ok := _c.mutation.AllowLive(); ok {
|
||||
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
|
||||
_node.AllowLive = value
|
||||
}
|
||||
if value, ok := _c.mutation.RequireOauthOnly(); ok {
|
||||
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
|
||||
_node.RequireOauthOnly = value
|
||||
@@ -1319,6 +1446,26 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldRpmLimit, field.TypeInt, value)
|
||||
_node.RpmLimit = value
|
||||
}
|
||||
if value, ok := _c.mutation.MaxReasoningEffort(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
|
||||
_node.MaxReasoningEffort = value
|
||||
}
|
||||
if value, ok := _c.mutation.ReasoningEffortMappings(); ok {
|
||||
_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,
|
||||
@@ -2177,6 +2324,18 @@ func (u *GroupUpsert) UpdateAllowMessagesDispatch() *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetAllowLive sets the "allow_live" field.
|
||||
func (u *GroupUpsert) SetAllowLive(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldAllowLive, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateAllowLive sets the "allow_live" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateAllowLive() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldAllowLive)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (u *GroupUpsert) SetRequireOauthOnly(v bool) *GroupUpsert {
|
||||
u.Set(group.FieldRequireOauthOnly, v)
|
||||
@@ -2255,6 +2414,78 @@ func (u *GroupUpsert) AddRpmLimit(v int) *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffort sets the "max_reasoning_effort" field.
|
||||
func (u *GroupUpsert) SetMaxReasoningEffort(v string) *GroupUpsert {
|
||||
u.Set(group.FieldMaxReasoningEffort, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateMaxReasoningEffort sets the "max_reasoning_effort" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateMaxReasoningEffort() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldMaxReasoningEffort)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (u *GroupUpsert) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsert {
|
||||
u.Set(group.FieldReasoningEffortMappings, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateReasoningEffortMappings sets the "reasoning_effort_mappings" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateReasoningEffortMappings() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldReasoningEffortMappings)
|
||||
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:
|
||||
//
|
||||
@@ -3122,6 +3353,20 @@ func (u *GroupUpsertOne) UpdateAllowMessagesDispatch() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetAllowLive sets the "allow_live" field.
|
||||
func (u *GroupUpsertOne) SetAllowLive(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAllowLive(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAllowLive sets the "allow_live" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateAllowLive() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAllowLive()
|
||||
})
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (u *GroupUpsertOne) SetRequireOauthOnly(v bool) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -3213,6 +3458,90 @@ func (u *GroupUpsertOne) UpdateRpmLimit() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffort sets the "max_reasoning_effort" field.
|
||||
func (u *GroupUpsertOne) SetMaxReasoningEffort(v string) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetMaxReasoningEffort(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateMaxReasoningEffort sets the "max_reasoning_effort" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateMaxReasoningEffort() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateMaxReasoningEffort()
|
||||
})
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (u *GroupUpsertOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetReasoningEffortMappings(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateReasoningEffortMappings sets the "reasoning_effort_mappings" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateReasoningEffortMappings() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateReasoningEffortMappings()
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -4246,6 +4575,20 @@ func (u *GroupUpsertBulk) UpdateAllowMessagesDispatch() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetAllowLive sets the "allow_live" field.
|
||||
func (u *GroupUpsertBulk) SetAllowLive(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetAllowLive(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAllowLive sets the "allow_live" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateAllowLive() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateAllowLive()
|
||||
})
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (u *GroupUpsertBulk) SetRequireOauthOnly(v bool) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
@@ -4337,6 +4680,90 @@ func (u *GroupUpsertBulk) UpdateRpmLimit() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffort sets the "max_reasoning_effort" field.
|
||||
func (u *GroupUpsertBulk) SetMaxReasoningEffort(v string) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetMaxReasoningEffort(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateMaxReasoningEffort sets the "max_reasoning_effort" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateMaxReasoningEffort() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateMaxReasoningEffort()
|
||||
})
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (u *GroupUpsertBulk) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetReasoningEffortMappings(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateReasoningEffortMappings sets the "reasoning_effort_mappings" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateReasoningEffortMappings() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateReasoningEffortMappings()
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -822,6 +822,20 @@ func (_u *GroupUpdate) SetNillableAllowMessagesDispatch(v *bool) *GroupUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAllowLive sets the "allow_live" field.
|
||||
func (_u *GroupUpdate) SetAllowLive(v bool) *GroupUpdate {
|
||||
_u.mutation.SetAllowLive(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAllowLive sets the "allow_live" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableAllowLive(v *bool) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetAllowLive(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (_u *GroupUpdate) SetRequireOauthOnly(v bool) *GroupUpdate {
|
||||
_u.mutation.SetRequireOauthOnly(v)
|
||||
@@ -913,6 +927,88 @@ func (_u *GroupUpdate) AddRpmLimit(v int) *GroupUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffort sets the "max_reasoning_effort" field.
|
||||
func (_u *GroupUpdate) SetMaxReasoningEffort(v string) *GroupUpdate {
|
||||
_u.mutation.SetMaxReasoningEffort(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMaxReasoningEffort sets the "max_reasoning_effort" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableMaxReasoningEffort(v *string) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetMaxReasoningEffort(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (_u *GroupUpdate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdate {
|
||||
_u.mutation.SetReasoningEffortMappings(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AppendReasoningEffortMappings appends value to the "reasoning_effort_mappings" field.
|
||||
func (_u *GroupUpdate) AppendReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdate {
|
||||
_u.mutation.AppendReasoningEffortMappings(v)
|
||||
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...)
|
||||
@@ -1213,6 +1309,11 @@ func (_u *GroupUpdate) check() error {
|
||||
return &ValidationError{Name: "default_mapped_model", err: fmt.Errorf(`ent: validator failed for field "Group.default_mapped_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.MaxReasoningEffort(); ok {
|
||||
if err := group.MaxReasoningEffortValidator(v); err != nil {
|
||||
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1464,6 +1565,9 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.AllowMessagesDispatch(); ok {
|
||||
_spec.SetField(group.FieldAllowMessagesDispatch, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AllowLive(); ok {
|
||||
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RequireOauthOnly(); ok {
|
||||
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
|
||||
}
|
||||
@@ -1485,6 +1589,32 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.AddedRpmLimit(); ok {
|
||||
_spec.AddField(group.FieldRpmLimit, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
|
||||
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AppendedReasoningEffortMappings(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
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,
|
||||
@@ -2585,6 +2715,20 @@ func (_u *GroupUpdateOne) SetNillableAllowMessagesDispatch(v *bool) *GroupUpdate
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAllowLive sets the "allow_live" field.
|
||||
func (_u *GroupUpdateOne) SetAllowLive(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetAllowLive(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableAllowLive sets the "allow_live" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableAllowLive(v *bool) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetAllowLive(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetRequireOauthOnly sets the "require_oauth_only" field.
|
||||
func (_u *GroupUpdateOne) SetRequireOauthOnly(v bool) *GroupUpdateOne {
|
||||
_u.mutation.SetRequireOauthOnly(v)
|
||||
@@ -2676,6 +2820,88 @@ func (_u *GroupUpdateOne) AddRpmLimit(v int) *GroupUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMaxReasoningEffort sets the "max_reasoning_effort" field.
|
||||
func (_u *GroupUpdateOne) SetMaxReasoningEffort(v string) *GroupUpdateOne {
|
||||
_u.mutation.SetMaxReasoningEffort(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMaxReasoningEffort sets the "max_reasoning_effort" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableMaxReasoningEffort(v *string) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetMaxReasoningEffort(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
|
||||
func (_u *GroupUpdateOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdateOne {
|
||||
_u.mutation.SetReasoningEffortMappings(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// AppendReasoningEffortMappings appends value to the "reasoning_effort_mappings" field.
|
||||
func (_u *GroupUpdateOne) AppendReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdateOne {
|
||||
_u.mutation.AppendReasoningEffortMappings(v)
|
||||
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...)
|
||||
@@ -2989,6 +3215,11 @@ func (_u *GroupUpdateOne) check() error {
|
||||
return &ValidationError{Name: "default_mapped_model", err: fmt.Errorf(`ent: validator failed for field "Group.default_mapped_model": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.MaxReasoningEffort(); ok {
|
||||
if err := group.MaxReasoningEffortValidator(v); err != nil {
|
||||
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3257,6 +3488,9 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if value, ok := _u.mutation.AllowMessagesDispatch(); ok {
|
||||
_spec.SetField(group.FieldAllowMessagesDispatch, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AllowLive(); ok {
|
||||
_spec.SetField(group.FieldAllowLive, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.RequireOauthOnly(); ok {
|
||||
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
|
||||
}
|
||||
@@ -3278,6 +3512,32 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if value, ok := _u.mutation.AddedRpmLimit(); ok {
|
||||
_spec.AddField(group.FieldRpmLimit, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
|
||||
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
|
||||
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AppendedReasoningEffortMappings(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
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,
|
||||
|
||||
@@ -177,6 +177,18 @@ func (f ChannelMonitorRequestTemplateFunc) Mutate(ctx context.Context, m ent.Mut
|
||||
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.ChannelMonitorRequestTemplateMutation", m)
|
||||
}
|
||||
|
||||
// The CompositeModelRouteFunc type is an adapter to allow the use of ordinary
|
||||
// function as CompositeModelRoute mutator.
|
||||
type CompositeModelRouteFunc func(context.Context, *ent.CompositeModelRouteMutation) (ent.Value, error)
|
||||
|
||||
// Mutate calls f(ctx, m).
|
||||
func (f CompositeModelRouteFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
|
||||
if mv, ok := m.(*ent.CompositeModelRouteMutation); ok {
|
||||
return f(ctx, mv)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.CompositeModelRouteMutation", m)
|
||||
}
|
||||
|
||||
// The ErrorPassthroughRuleFunc type is an adapter to allow the use of ordinary
|
||||
// function as ErrorPassthroughRule mutator.
|
||||
type ErrorPassthroughRuleFunc func(context.Context, *ent.ErrorPassthroughRuleMutation) (ent.Value, error)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
|
||||
@@ -483,6 +484,33 @@ func (f TraverseChannelMonitorRequestTemplate) Traverse(ctx context.Context, q e
|
||||
return fmt.Errorf("unexpected query type %T. expect *ent.ChannelMonitorRequestTemplateQuery", q)
|
||||
}
|
||||
|
||||
// The CompositeModelRouteFunc type is an adapter to allow the use of ordinary function as a Querier.
|
||||
type CompositeModelRouteFunc func(context.Context, *ent.CompositeModelRouteQuery) (ent.Value, error)
|
||||
|
||||
// Query calls f(ctx, q).
|
||||
func (f CompositeModelRouteFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
|
||||
if q, ok := q.(*ent.CompositeModelRouteQuery); ok {
|
||||
return f(ctx, q)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected query type %T. expect *ent.CompositeModelRouteQuery", q)
|
||||
}
|
||||
|
||||
// The TraverseCompositeModelRoute type is an adapter to allow the use of ordinary function as Traverser.
|
||||
type TraverseCompositeModelRoute func(context.Context, *ent.CompositeModelRouteQuery) error
|
||||
|
||||
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
|
||||
func (f TraverseCompositeModelRoute) Intercept(next ent.Querier) ent.Querier {
|
||||
return next
|
||||
}
|
||||
|
||||
// Traverse calls f(ctx, q).
|
||||
func (f TraverseCompositeModelRoute) Traverse(ctx context.Context, q ent.Query) error {
|
||||
if q, ok := q.(*ent.CompositeModelRouteQuery); ok {
|
||||
return f(ctx, q)
|
||||
}
|
||||
return fmt.Errorf("unexpected query type %T. expect *ent.CompositeModelRouteQuery", q)
|
||||
}
|
||||
|
||||
// The ErrorPassthroughRuleFunc type is an adapter to allow the use of ordinary function as a Querier.
|
||||
type ErrorPassthroughRuleFunc func(context.Context, *ent.ErrorPassthroughRuleQuery) (ent.Value, error)
|
||||
|
||||
@@ -1162,6 +1190,8 @@ func NewQuery(q ent.Query) (Query, error) {
|
||||
return &query[*ent.ChannelMonitorHistoryQuery, predicate.ChannelMonitorHistory, channelmonitorhistory.OrderOption]{typ: ent.TypeChannelMonitorHistory, tq: q}, nil
|
||||
case *ent.ChannelMonitorRequestTemplateQuery:
|
||||
return &query[*ent.ChannelMonitorRequestTemplateQuery, predicate.ChannelMonitorRequestTemplate, channelmonitorrequesttemplate.OrderOption]{typ: ent.TypeChannelMonitorRequestTemplate, tq: q}, nil
|
||||
case *ent.CompositeModelRouteQuery:
|
||||
return &query[*ent.CompositeModelRouteQuery, predicate.CompositeModelRoute, compositemodelroute.OrderOption]{typ: ent.TypeCompositeModelRoute, tq: q}, nil
|
||||
case *ent.ErrorPassthroughRuleQuery:
|
||||
return &query[*ent.ErrorPassthroughRuleQuery, predicate.ErrorPassthroughRule, errorpassthroughrule.OrderOption]{typ: ent.TypeErrorPassthroughRule, tq: q}, nil
|
||||
case *ent.GroupQuery:
|
||||
|
||||
@@ -793,6 +793,68 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
// CompositeModelRoutesColumns holds the columns for the "composite_model_routes" table.
|
||||
CompositeModelRoutesColumns = []*schema.Column{
|
||||
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||
{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: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "public_model", Type: field.TypeString, Size: 200},
|
||||
{Name: "match_type", Type: field.TypeString, Size: 20, Default: "exact"},
|
||||
{Name: "target_platform", Type: field.TypeString, Size: 50, Default: "openai"},
|
||||
{Name: "upstream_model", Type: field.TypeString, Size: 200, Default: ""},
|
||||
{Name: "endpoint", Type: field.TypeString, Size: 50, Default: "any"},
|
||||
{Name: "priority", Type: field.TypeInt, Default: 100},
|
||||
{Name: "enabled", Type: field.TypeBool, Default: true},
|
||||
{Name: "notes", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
|
||||
{Name: "group_id", Type: field.TypeInt64},
|
||||
}
|
||||
// CompositeModelRoutesTable holds the schema information for the "composite_model_routes" table.
|
||||
CompositeModelRoutesTable = &schema.Table{
|
||||
Name: "composite_model_routes",
|
||||
Columns: CompositeModelRoutesColumns,
|
||||
PrimaryKey: []*schema.Column{CompositeModelRoutesColumns[0]},
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "composite_model_routes_groups_group",
|
||||
Columns: []*schema.Column{CompositeModelRoutesColumns[12]},
|
||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
},
|
||||
Indexes: []*schema.Index{
|
||||
{
|
||||
Name: "compositemodelroute_group_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{CompositeModelRoutesColumns[12]},
|
||||
},
|
||||
{
|
||||
Name: "compositemodelroute_group_id_enabled",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{CompositeModelRoutesColumns[12], CompositeModelRoutesColumns[10]},
|
||||
},
|
||||
{
|
||||
Name: "compositemodelroute_group_id_endpoint",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{CompositeModelRoutesColumns[12], CompositeModelRoutesColumns[8]},
|
||||
},
|
||||
{
|
||||
Name: "compositemodelroute_group_id_target_platform",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{CompositeModelRoutesColumns[12], CompositeModelRoutesColumns[6]},
|
||||
},
|
||||
{
|
||||
Name: "compositemodelroute_deleted_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{CompositeModelRoutesColumns[3]},
|
||||
},
|
||||
{
|
||||
Name: "compositemodelroute_priority",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{CompositeModelRoutesColumns[9]},
|
||||
},
|
||||
},
|
||||
}
|
||||
// ErrorPassthroughRulesColumns holds the columns for the "error_passthrough_rules" table.
|
||||
ErrorPassthroughRulesColumns = []*schema.Column{
|
||||
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||
@@ -876,12 +938,18 @@ var (
|
||||
{Name: "supported_model_scopes", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "sort_order", Type: field.TypeInt, Default: 0},
|
||||
{Name: "allow_messages_dispatch", Type: field.TypeBool, Default: false},
|
||||
{Name: "allow_live", Type: field.TypeBool, Default: false},
|
||||
{Name: "require_oauth_only", Type: field.TypeBool, Default: false},
|
||||
{Name: "require_privacy_set", Type: field.TypeBool, Default: false},
|
||||
{Name: "default_mapped_model", Type: field.TypeString, Size: 100, Default: ""},
|
||||
{Name: "messages_dispatch_model_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "models_list_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "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{
|
||||
@@ -1553,6 +1621,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},
|
||||
@@ -1603,31 +1673,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,
|
||||
},
|
||||
@@ -1636,32 +1706,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",
|
||||
@@ -1681,17 +1751,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]},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2012,6 +2082,7 @@ var (
|
||||
ChannelMonitorDailyRollupsTable,
|
||||
ChannelMonitorHistoriesTable,
|
||||
ChannelMonitorRequestTemplatesTable,
|
||||
CompositeModelRoutesTable,
|
||||
ErrorPassthroughRulesTable,
|
||||
GroupsTable,
|
||||
IdempotencyRecordsTable,
|
||||
@@ -2095,6 +2166,10 @@ func init() {
|
||||
ChannelMonitorRequestTemplatesTable.Annotation = &entsql.Annotation{
|
||||
Table: "channel_monitor_request_templates",
|
||||
}
|
||||
CompositeModelRoutesTable.ForeignKeys[0].RefTable = GroupsTable
|
||||
CompositeModelRoutesTable.Annotation = &entsql.Annotation{
|
||||
Table: "composite_model_routes",
|
||||
}
|
||||
ErrorPassthroughRulesTable.Annotation = &entsql.Annotation{
|
||||
Table: "error_passthrough_rules",
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ type ChannelMonitorHistory func(*sql.Selector)
|
||||
// ChannelMonitorRequestTemplate is the predicate function for channelmonitorrequesttemplate builders.
|
||||
type ChannelMonitorRequestTemplate func(*sql.Selector)
|
||||
|
||||
// CompositeModelRoute is the predicate function for compositemodelroute builders.
|
||||
type CompositeModelRoute func(*sql.Selector)
|
||||
|
||||
// ErrorPassthroughRule is the predicate function for errorpassthroughrule builders.
|
||||
type ErrorPassthroughRule func(*sql.Selector)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorrequesttemplate"
|
||||
"github.com/Wei-Shaw/sub2api/ent/compositemodelroute"
|
||||
"github.com/Wei-Shaw/sub2api/ent/errorpassthroughrule"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
"github.com/Wei-Shaw/sub2api/ent/idempotencyrecord"
|
||||
@@ -865,6 +866,75 @@ func init() {
|
||||
channelmonitorrequesttemplate.DefaultBodyOverrideMode = channelmonitorrequesttemplateDescBodyOverrideMode.Default.(string)
|
||||
// channelmonitorrequesttemplate.BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save.
|
||||
channelmonitorrequesttemplate.BodyOverrideModeValidator = channelmonitorrequesttemplateDescBodyOverrideMode.Validators[0].(func(string) error)
|
||||
compositemodelrouteMixin := schema.CompositeModelRoute{}.Mixin()
|
||||
compositemodelrouteMixinHooks1 := compositemodelrouteMixin[1].Hooks()
|
||||
compositemodelroute.Hooks[0] = compositemodelrouteMixinHooks1[0]
|
||||
compositemodelrouteMixinInters1 := compositemodelrouteMixin[1].Interceptors()
|
||||
compositemodelroute.Interceptors[0] = compositemodelrouteMixinInters1[0]
|
||||
compositemodelrouteMixinFields0 := compositemodelrouteMixin[0].Fields()
|
||||
_ = compositemodelrouteMixinFields0
|
||||
compositemodelrouteFields := schema.CompositeModelRoute{}.Fields()
|
||||
_ = compositemodelrouteFields
|
||||
// compositemodelrouteDescCreatedAt is the schema descriptor for created_at field.
|
||||
compositemodelrouteDescCreatedAt := compositemodelrouteMixinFields0[0].Descriptor()
|
||||
// compositemodelroute.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
compositemodelroute.DefaultCreatedAt = compositemodelrouteDescCreatedAt.Default.(func() time.Time)
|
||||
// compositemodelrouteDescUpdatedAt is the schema descriptor for updated_at field.
|
||||
compositemodelrouteDescUpdatedAt := compositemodelrouteMixinFields0[1].Descriptor()
|
||||
// compositemodelroute.DefaultUpdatedAt holds the default value on creation for the updated_at field.
|
||||
compositemodelroute.DefaultUpdatedAt = compositemodelrouteDescUpdatedAt.Default.(func() time.Time)
|
||||
// compositemodelroute.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
||||
compositemodelroute.UpdateDefaultUpdatedAt = compositemodelrouteDescUpdatedAt.UpdateDefault.(func() time.Time)
|
||||
// compositemodelrouteDescPublicModel is the schema descriptor for public_model field.
|
||||
compositemodelrouteDescPublicModel := compositemodelrouteFields[1].Descriptor()
|
||||
// compositemodelroute.PublicModelValidator is a validator for the "public_model" field. It is called by the builders before save.
|
||||
compositemodelroute.PublicModelValidator = func() func(string) error {
|
||||
validators := compositemodelrouteDescPublicModel.Validators
|
||||
fns := [...]func(string) error{
|
||||
validators[0].(func(string) error),
|
||||
validators[1].(func(string) error),
|
||||
}
|
||||
return func(public_model string) error {
|
||||
for _, fn := range fns {
|
||||
if err := fn(public_model); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
// compositemodelrouteDescMatchType is the schema descriptor for match_type field.
|
||||
compositemodelrouteDescMatchType := compositemodelrouteFields[2].Descriptor()
|
||||
// compositemodelroute.DefaultMatchType holds the default value on creation for the match_type field.
|
||||
compositemodelroute.DefaultMatchType = compositemodelrouteDescMatchType.Default.(string)
|
||||
// compositemodelroute.MatchTypeValidator is a validator for the "match_type" field. It is called by the builders before save.
|
||||
compositemodelroute.MatchTypeValidator = compositemodelrouteDescMatchType.Validators[0].(func(string) error)
|
||||
// compositemodelrouteDescTargetPlatform is the schema descriptor for target_platform field.
|
||||
compositemodelrouteDescTargetPlatform := compositemodelrouteFields[3].Descriptor()
|
||||
// compositemodelroute.DefaultTargetPlatform holds the default value on creation for the target_platform field.
|
||||
compositemodelroute.DefaultTargetPlatform = compositemodelrouteDescTargetPlatform.Default.(string)
|
||||
// compositemodelroute.TargetPlatformValidator is a validator for the "target_platform" field. It is called by the builders before save.
|
||||
compositemodelroute.TargetPlatformValidator = compositemodelrouteDescTargetPlatform.Validators[0].(func(string) error)
|
||||
// compositemodelrouteDescUpstreamModel is the schema descriptor for upstream_model field.
|
||||
compositemodelrouteDescUpstreamModel := compositemodelrouteFields[4].Descriptor()
|
||||
// compositemodelroute.DefaultUpstreamModel holds the default value on creation for the upstream_model field.
|
||||
compositemodelroute.DefaultUpstreamModel = compositemodelrouteDescUpstreamModel.Default.(string)
|
||||
// compositemodelroute.UpstreamModelValidator is a validator for the "upstream_model" field. It is called by the builders before save.
|
||||
compositemodelroute.UpstreamModelValidator = compositemodelrouteDescUpstreamModel.Validators[0].(func(string) error)
|
||||
// compositemodelrouteDescEndpoint is the schema descriptor for endpoint field.
|
||||
compositemodelrouteDescEndpoint := compositemodelrouteFields[5].Descriptor()
|
||||
// compositemodelroute.DefaultEndpoint holds the default value on creation for the endpoint field.
|
||||
compositemodelroute.DefaultEndpoint = compositemodelrouteDescEndpoint.Default.(string)
|
||||
// compositemodelroute.EndpointValidator is a validator for the "endpoint" field. It is called by the builders before save.
|
||||
compositemodelroute.EndpointValidator = compositemodelrouteDescEndpoint.Validators[0].(func(string) error)
|
||||
// compositemodelrouteDescPriority is the schema descriptor for priority field.
|
||||
compositemodelrouteDescPriority := compositemodelrouteFields[6].Descriptor()
|
||||
// compositemodelroute.DefaultPriority holds the default value on creation for the priority field.
|
||||
compositemodelroute.DefaultPriority = compositemodelrouteDescPriority.Default.(int)
|
||||
// compositemodelrouteDescEnabled is the schema descriptor for enabled field.
|
||||
compositemodelrouteDescEnabled := compositemodelrouteFields[7].Descriptor()
|
||||
// compositemodelroute.DefaultEnabled holds the default value on creation for the enabled field.
|
||||
compositemodelroute.DefaultEnabled = compositemodelrouteDescEnabled.Default.(bool)
|
||||
errorpassthroughruleMixin := schema.ErrorPassthroughRule{}.Mixin()
|
||||
errorpassthroughruleMixinFields0 := errorpassthroughruleMixin[0].Fields()
|
||||
_ = errorpassthroughruleMixinFields0
|
||||
@@ -1071,32 +1141,58 @@ func init() {
|
||||
groupDescAllowMessagesDispatch := groupFields[39].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()
|
||||
// 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[40].Descriptor()
|
||||
groupDescRequireOauthOnly := groupFields[41].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[41].Descriptor()
|
||||
groupDescRequirePrivacySet := groupFields[42].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[42].Descriptor()
|
||||
groupDescDefaultMappedModel := groupFields[43].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[43].Descriptor()
|
||||
groupDescMessagesDispatchModelConfig := groupFields[44].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[44].Descriptor()
|
||||
groupDescModelsListConfig := groupFields[45].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[45].Descriptor()
|
||||
groupDescRpmLimit := groupFields[46].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()
|
||||
// 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()
|
||||
// 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[49].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[50].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[51].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
|
||||
@@ -1886,124 +1982,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()
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/schema"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/index"
|
||||
)
|
||||
|
||||
// CompositeModelRoute holds model routing aliases for composite groups.
|
||||
type CompositeModelRoute struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
func (CompositeModelRoute) Annotations() []schema.Annotation {
|
||||
return []schema.Annotation{
|
||||
entsql.Annotation{Table: "composite_model_routes"},
|
||||
}
|
||||
}
|
||||
|
||||
func (CompositeModelRoute) Mixin() []ent.Mixin {
|
||||
return []ent.Mixin{
|
||||
mixins.TimeMixin{},
|
||||
mixins.SoftDeleteMixin{},
|
||||
}
|
||||
}
|
||||
|
||||
func (CompositeModelRoute) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Int64("group_id"),
|
||||
field.String("public_model").
|
||||
MaxLen(200).
|
||||
NotEmpty().
|
||||
Comment("Client-facing model identifier or prefix."),
|
||||
field.String("match_type").
|
||||
MaxLen(20).
|
||||
Default("exact").
|
||||
Comment("exact or prefix."),
|
||||
field.String("target_platform").
|
||||
MaxLen(50).
|
||||
Default(domain.PlatformOpenAI).
|
||||
Comment("Concrete provider platform."),
|
||||
field.String("upstream_model").
|
||||
MaxLen(200).
|
||||
Default("").
|
||||
Comment("Provider model identifier; empty means public_model."),
|
||||
field.String("endpoint").
|
||||
MaxLen(50).
|
||||
Default("any").
|
||||
Comment("Endpoint scope such as any, messages, responses, chat_completions."),
|
||||
field.Int("priority").
|
||||
Default(100).
|
||||
Comment("Lower values win within the same match strength."),
|
||||
field.Bool("enabled").
|
||||
Default(true),
|
||||
field.String("notes").
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "text"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (CompositeModelRoute) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.To("group", Group.Type).
|
||||
Unique().
|
||||
Required().
|
||||
Field("group_id"),
|
||||
}
|
||||
}
|
||||
|
||||
func (CompositeModelRoute) Indexes() []ent.Index {
|
||||
return []ent.Index{
|
||||
index.Fields("group_id"),
|
||||
index.Fields("group_id", "enabled"),
|
||||
index.Fields("group_id", "endpoint"),
|
||||
index.Fields("group_id", "target_platform"),
|
||||
index.Fields("deleted_at"),
|
||||
index.Fields("priority"),
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,9 @@ func (Group) Fields() []ent.Field {
|
||||
field.Bool("allow_messages_dispatch").
|
||||
Default(false).
|
||||
Comment("是否允许 /v1/messages 调度到此 OpenAI 分组"),
|
||||
field.Bool("allow_live").
|
||||
Default(false).
|
||||
Comment("是否允许此 OpenAI 分组访问 Live 接口"),
|
||||
field.Bool("require_oauth_only").
|
||||
Default(false).
|
||||
Comment("仅允许非 apikey 类型账号关联到此分组"),
|
||||
@@ -221,6 +224,30 @@ func (Group) Fields() []ent.Field {
|
||||
field.Int("rpm_limit").
|
||||
Default(0).
|
||||
Comment("分组 RPM 上限,0 表示不限制;设置后接管该分组用户的限流"),
|
||||
|
||||
// OpenAI/Codex 请求的推理强度上限(空字符串表示不限制)。
|
||||
field.String("max_reasoning_effort").
|
||||
MaxLen(20).
|
||||
Default("").
|
||||
Comment("OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max"),
|
||||
field.JSON("reasoning_effort_mappings", []domain.ReasoningEffortMapping{}).
|
||||
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("计费层级标签"),
|
||||
|
||||
@@ -42,6 +42,8 @@ type Tx struct {
|
||||
ChannelMonitorHistory *ChannelMonitorHistoryClient
|
||||
// ChannelMonitorRequestTemplate is the client for interacting with the ChannelMonitorRequestTemplate builders.
|
||||
ChannelMonitorRequestTemplate *ChannelMonitorRequestTemplateClient
|
||||
// CompositeModelRoute is the client for interacting with the CompositeModelRoute builders.
|
||||
CompositeModelRoute *CompositeModelRouteClient
|
||||
// ErrorPassthroughRule is the client for interacting with the ErrorPassthroughRule builders.
|
||||
ErrorPassthroughRule *ErrorPassthroughRuleClient
|
||||
// Group is the client for interacting with the Group builders.
|
||||
@@ -235,6 +237,7 @@ func (tx *Tx) init() {
|
||||
tx.ChannelMonitorDailyRollup = NewChannelMonitorDailyRollupClient(tx.config)
|
||||
tx.ChannelMonitorHistory = NewChannelMonitorHistoryClient(tx.config)
|
||||
tx.ChannelMonitorRequestTemplate = NewChannelMonitorRequestTemplateClient(tx.config)
|
||||
tx.CompositeModelRoute = NewCompositeModelRouteClient(tx.config)
|
||||
tx.ErrorPassthroughRule = NewErrorPassthroughRuleClient(tx.config)
|
||||
tx.Group = NewGroupClient(tx.config)
|
||||
tx.IdempotencyRecord = NewIdempotencyRecordClient(tx.config)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ go 1.26.5
|
||||
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
|
||||
@@ -16,11 +19,12 @@ require (
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/dgraph-io/ristretto v0.2.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/go-webauthn/webauthn v0.17.4
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/imroc/req/v3 v3.57.0
|
||||
github.com/imroc/req/v3 v3.59.0
|
||||
github.com/klauspost/compress v1.18.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
@@ -34,6 +38,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
|
||||
@@ -42,12 +48,12 @@ require (
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||
github.com/zeromicro/go-zero v1.9.4
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/image v0.39.0
|
||||
golang.org/x/mod v0.35.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.43.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/image v0.41.0
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/term v0.44.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.44.3
|
||||
@@ -59,6 +65,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
|
||||
@@ -80,6 +90,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
|
||||
@@ -97,6 +108,7 @@ require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -106,9 +118,12 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
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/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
|
||||
@@ -137,11 +152,12 @@ require (
|
||||
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
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/quic-go/quic-go v0.60.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
@@ -158,28 +174,32 @@ require (
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 // indirect
|
||||
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
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.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.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.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.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
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
|
||||
gopkg.in/ini.v1 v1.67.0 // 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=
|
||||
@@ -77,11 +125,16 @@ 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 +173,9 @@ 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.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=
|
||||
@@ -128,6 +184,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
@@ -153,25 +211,51 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
|
||||
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
|
||||
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/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=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
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=
|
||||
@@ -185,8 +269,8 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
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.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
||||
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
||||
github.com/imroc/req/v3 v3.59.0 h1:PqKhJHyBmJYob47LVuTHwRZE00ZO6icbLHe5Zra13jo=
|
||||
github.com/imroc/req/v3 v3.59.0/go.mod h1:cwLwwaE90Pq6ZMmKuHA9FivJbg3avnCH8/RA5lUUs74=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -195,8 +279,10 @@ 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/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=
|
||||
@@ -205,6 +291,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=
|
||||
@@ -222,8 +310,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
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=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
||||
@@ -251,14 +337,15 @@ 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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
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=
|
||||
@@ -267,6 +354,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -278,22 +367,23 @@ 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=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
@@ -314,6 +404,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=
|
||||
@@ -322,18 +415,18 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -347,6 +440,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=
|
||||
@@ -364,6 +461,11 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tiktoken-go/tokenizer v0.8.0 h1:drHWno2Zx3eAm/hk/LmvBKXPpSImB7BRyh/ru4+3Q7Y=
|
||||
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=
|
||||
@@ -374,8 +476,13 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
|
||||
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=
|
||||
@@ -386,22 +493,22 @@ github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWB
|
||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||
github.com/zeromicro/go-zero v1.9.4 h1:aRLFoISqAYijABtkbliQC5SsI5TbizJpQvoHc9xup8k=
|
||||
github.com/zeromicro/go-zero v1.9.4/go.mod h1:a17JOTch25SWxBcUgJZYps60hygK3pIYdw7nGwlcS38=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
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/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.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
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/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=
|
||||
@@ -419,59 +526,189 @@ 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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
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.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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-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-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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
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=
|
||||
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.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.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/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 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 (
|
||||
@@ -73,6 +73,7 @@ type Config struct {
|
||||
Ops OpsConfig `mapstructure:"ops"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Totp TotpConfig `mapstructure:"totp"`
|
||||
WebAuthn WebAuthnConfig `mapstructure:"webauthn"`
|
||||
LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
|
||||
WeChat WeChatConnectConfig `mapstructure:"wechat_connect"`
|
||||
OIDC OIDCConnectConfig `mapstructure:"oidc_connect"`
|
||||
@@ -686,6 +687,16 @@ type CORSConfig struct {
|
||||
AllowCredentials bool `mapstructure:"allow_credentials"`
|
||||
}
|
||||
|
||||
// WebAuthnConfig configures this deployment as a WebAuthn relying party.
|
||||
// RPID and RPOrigins are security boundaries and must never be inferred from
|
||||
// untrusted request Host or Origin headers.
|
||||
type WebAuthnConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
RPDisplayName string `mapstructure:"rp_display_name"`
|
||||
RPID string `mapstructure:"rp_id"`
|
||||
RPOrigins []string `mapstructure:"rp_origins"`
|
||||
}
|
||||
|
||||
const MaxForwardedClientIPHeaders = 16
|
||||
|
||||
type ForwardedClientIPSettings struct {
|
||||
@@ -896,6 +907,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"`
|
||||
@@ -913,10 +936,14 @@ type GatewayConfig struct {
|
||||
OpenAICompactModel string `mapstructure:"openai_compact_model"`
|
||||
// OpenAIWS: OpenAI Responses WebSocket 配置(默认开启,可按需回滚到 HTTP)
|
||||
OpenAIWS GatewayOpenAIWSConfig `mapstructure:"openai_ws"`
|
||||
// Live: ChatGPT Frameless Live 会话配置。
|
||||
Live GatewayLiveConfig `mapstructure:"live"`
|
||||
// OpenAIScheduler: OpenAI 高级调度器粘性逃逸配置
|
||||
OpenAIScheduler GatewayOpenAISchedulerConfig `mapstructure:"openai_scheduler"`
|
||||
// OpenAIHTTP2: OpenAI HTTP 上游协议策略(默认启用 HTTP/2,可按代理能力回退 HTTP/1.1)
|
||||
OpenAIHTTP2 GatewayOpenAIHTTP2Config `mapstructure:"openai_http2"`
|
||||
// OpenAIProxyStreamCircuit: Responses SSE 代理断流熔断策略。
|
||||
OpenAIProxyStreamCircuit GatewayOpenAIProxyStreamCircuitConfig `mapstructure:"openai_proxy_stream_circuit"`
|
||||
// ImageConcurrency: 图片生成独立并发限制配置(默认关闭)
|
||||
ImageConcurrency ImageConcurrencyConfig `mapstructure:"image_concurrency"`
|
||||
|
||||
@@ -997,6 +1024,11 @@ type GatewayConfig struct {
|
||||
UserMessageQueue UserMessageQueueConfig `mapstructure:"user_message_queue"`
|
||||
}
|
||||
|
||||
type GatewayLiveConfig struct {
|
||||
// MaxSessionDurationSeconds 是 Live 会话的硬上限。
|
||||
MaxSessionDurationSeconds int `mapstructure:"max_session_duration_seconds"`
|
||||
}
|
||||
|
||||
// GatewayOpenAIHTTP2Config OpenAI HTTP 上游协议配置。
|
||||
// 默认启用 HTTP/2;在部分代理不兼容时按策略回退 HTTP/1.1。
|
||||
type GatewayOpenAIHTTP2Config struct {
|
||||
@@ -1012,6 +1044,19 @@ type GatewayOpenAIHTTP2Config struct {
|
||||
FallbackTTLSeconds int `mapstructure:"fallback_ttl_seconds"`
|
||||
}
|
||||
|
||||
// GatewayOpenAIProxyStreamCircuitConfig controls the bounded, in-process
|
||||
// proxy-ID circuit used for incomplete OpenAI Responses SSE streams.
|
||||
type GatewayOpenAIProxyStreamCircuitConfig struct {
|
||||
// Disabled: 完全关闭代理断流熔断(默认开启)。
|
||||
Disabled bool `mapstructure:"disabled"`
|
||||
// FailureThreshold: 统计窗口内多少次断流后隔离代理。
|
||||
FailureThreshold int `mapstructure:"failure_threshold"`
|
||||
// WindowSeconds: 断流统计窗口(秒)。
|
||||
WindowSeconds int `mapstructure:"window_seconds"`
|
||||
// TTLSeconds: 代理隔离持续时间(秒)。
|
||||
TTLSeconds int `mapstructure:"ttl_seconds"`
|
||||
}
|
||||
|
||||
// UserMessageQueueConfig 用户消息串行队列配置
|
||||
// 用于 Anthropic OAuth/SetupToken 账号的用户消息串行化发送
|
||||
type UserMessageQueueConfig struct {
|
||||
@@ -1403,6 +1448,7 @@ func (d *DatabaseConfig) DSNWithTimezone(tz string) string {
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
// 连接池与超时配置(性能优化:可配置化连接池参数)
|
||||
@@ -1619,20 +1665,7 @@ func LoadForBootstrap() (*Config, error) {
|
||||
func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// Add config paths in priority order
|
||||
// 1. DATA_DIR environment variable (highest priority)
|
||||
if dataDir := os.Getenv("DATA_DIR"); dataDir != "" {
|
||||
viper.AddConfigPath(dataDir)
|
||||
}
|
||||
// 2. Docker data directory
|
||||
viper.AddConfigPath("/app/data")
|
||||
// 3. Current directory
|
||||
viper.AddConfigPath(".")
|
||||
// 4. Config subdirectory
|
||||
viper.AddConfigPath("./config")
|
||||
// 5. System config directory
|
||||
viper.AddConfigPath("/etc/sub2api")
|
||||
configureConfigSource(viper.SetConfigFile, viper.AddConfigPath)
|
||||
|
||||
// 环境变量支持
|
||||
viper.AutomaticEnv()
|
||||
@@ -1745,6 +1778,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 {
|
||||
@@ -1807,6 +1847,22 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func configureConfigSource(setConfigFile, addConfigPath func(string)) {
|
||||
if configFile := strings.TrimSpace(os.Getenv("CONFIG_FILE")); configFile != "" {
|
||||
setConfigFile(configFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Add config paths in priority order.
|
||||
if dataDir := strings.TrimSpace(os.Getenv("DATA_DIR")); dataDir != "" {
|
||||
addConfigPath(dataDir)
|
||||
}
|
||||
addConfigPath("/app/data")
|
||||
addConfigPath(".")
|
||||
addConfigPath("./config")
|
||||
addConfigPath("/etc/sub2api")
|
||||
}
|
||||
|
||||
func setDefaults() {
|
||||
viper.SetDefault("run_mode", RunModeStandard)
|
||||
|
||||
@@ -1851,12 +1907,21 @@ func setDefaults() {
|
||||
viper.SetDefault("cors.allowed_origins", []string{})
|
||||
viper.SetDefault("cors.allow_credentials", true)
|
||||
|
||||
// WebAuthn / Passkeys are opt-in because every deployment must explicitly
|
||||
// declare its relying-party domain and trusted browser origins.
|
||||
viper.SetDefault("webauthn.enabled", false)
|
||||
viper.SetDefault("webauthn.rp_display_name", "Sub2API")
|
||||
viper.SetDefault("webauthn.rp_id", "")
|
||||
viper.SetDefault("webauthn.rp_origins", []string{})
|
||||
|
||||
// Security
|
||||
viper.SetDefault("security.url_allowlist.enabled", false)
|
||||
viper.SetDefault("security.url_allowlist.upstream_hosts", []string{
|
||||
"api.openai.com",
|
||||
"api.anthropic.com",
|
||||
"api.kimi.com",
|
||||
"api.moonshot.ai",
|
||||
"api.moonshot.cn",
|
||||
"open.bigmodel.cn",
|
||||
"api.minimaxi.com",
|
||||
"generativelanguage.googleapis.com",
|
||||
@@ -1981,6 +2046,7 @@ func setDefaults() {
|
||||
// Redis
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", 6379)
|
||||
viper.SetDefault("redis.username", "")
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.db", 0)
|
||||
viper.SetDefault("redis.dial_timeout_seconds", 5)
|
||||
@@ -2170,9 +2236,12 @@ 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")
|
||||
viper.SetDefault("gateway.live.max_session_duration_seconds", 3600)
|
||||
// OpenAI Responses WebSocket(默认开启;可通过 force_http 紧急回滚)
|
||||
viper.SetDefault("gateway.openai_ws.enabled", true)
|
||||
viper.SetDefault("gateway.openai_ws.mode_router_v2_enabled", false)
|
||||
@@ -2236,6 +2305,10 @@ func setDefaults() {
|
||||
viper.SetDefault("gateway.openai_http2.fallback_error_threshold", 2)
|
||||
viper.SetDefault("gateway.openai_http2.fallback_window_seconds", 60)
|
||||
viper.SetDefault("gateway.openai_http2.fallback_ttl_seconds", 600)
|
||||
viper.SetDefault("gateway.openai_proxy_stream_circuit.disabled", false)
|
||||
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)
|
||||
viper.SetDefault("gateway.image_concurrency.enabled", false)
|
||||
viper.SetDefault("gateway.image_concurrency.max_concurrent_requests", 0)
|
||||
viper.SetDefault("gateway.image_concurrency.overflow_mode", ImageConcurrencyOverflowModeReject)
|
||||
@@ -2562,6 +2635,43 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
warnIfInsecureURL("server.frontend_url", c.Server.FrontendURL)
|
||||
}
|
||||
if c.WebAuthn.Enabled {
|
||||
c.WebAuthn.RPDisplayName = strings.TrimSpace(c.WebAuthn.RPDisplayName)
|
||||
c.WebAuthn.RPID = strings.ToLower(strings.TrimSpace(c.WebAuthn.RPID))
|
||||
c.WebAuthn.RPOrigins = normalizeStringSlice(c.WebAuthn.RPOrigins)
|
||||
if c.WebAuthn.RPDisplayName == "" {
|
||||
return fmt.Errorf("webauthn.rp_display_name is required when passkeys are enabled")
|
||||
}
|
||||
if c.WebAuthn.RPID == "" {
|
||||
return fmt.Errorf("webauthn.rp_id is required when passkeys are enabled")
|
||||
}
|
||||
if strings.Contains(c.WebAuthn.RPID, "://") || strings.ContainsAny(c.WebAuthn.RPID, "/:") {
|
||||
return fmt.Errorf("webauthn.rp_id must be a domain without scheme, port, or path")
|
||||
}
|
||||
if len(c.WebAuthn.RPOrigins) == 0 {
|
||||
return fmt.Errorf("webauthn.rp_origins must contain at least one origin when passkeys are enabled")
|
||||
}
|
||||
for i, origin := range c.WebAuthn.RPOrigins {
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
return fmt.Errorf("webauthn.rp_origins contains invalid origin %q", origin)
|
||||
}
|
||||
if u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "" {
|
||||
return fmt.Errorf("webauthn.rp_origins entry %q must not include userinfo, path, query, or fragment", origin)
|
||||
}
|
||||
u.Scheme = strings.ToLower(u.Scheme)
|
||||
u.Host = strings.ToLower(u.Host)
|
||||
host := strings.ToLower(u.Hostname())
|
||||
localDevelopment := host == "localhost" || host == "127.0.0.1" || host == "::1"
|
||||
if u.Scheme != "https" && (u.Scheme != "http" || !localDevelopment) {
|
||||
return fmt.Errorf("webauthn.rp_origins entry %q must use HTTPS (HTTP is allowed only for localhost)", origin)
|
||||
}
|
||||
if host != c.WebAuthn.RPID && !strings.HasSuffix(host, "."+c.WebAuthn.RPID) {
|
||||
return fmt.Errorf("webauthn.rp_origins entry %q is not within relying party ID %q", origin, c.WebAuthn.RPID)
|
||||
}
|
||||
c.WebAuthn.RPOrigins[i] = u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
if c.JWT.ExpireHour <= 0 {
|
||||
return fmt.Errorf("jwt.expire_hour must be positive")
|
||||
}
|
||||
@@ -3021,6 +3131,9 @@ func (c *Config) Validate() error {
|
||||
(c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds > 0 && c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds < 30) {
|
||||
return fmt.Errorf("gateway.openai_high_effort_first_output_timeout_seconds must be 0 or between 30-1800 seconds")
|
||||
}
|
||||
if c.Gateway.Live.MaxSessionDurationSeconds <= 0 {
|
||||
c.Gateway.Live.MaxSessionDurationSeconds = 3600
|
||||
}
|
||||
if strings.TrimSpace(c.Gateway.ConnectionPoolIsolation) != "" {
|
||||
switch c.Gateway.ConnectionPoolIsolation {
|
||||
case ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy:
|
||||
@@ -3229,6 +3342,15 @@ func (c *Config) Validate() error {
|
||||
if c.Gateway.OpenAIHTTP2.FallbackTTLSeconds < 0 {
|
||||
return fmt.Errorf("gateway.openai_http2.fallback_ttl_seconds must be non-negative")
|
||||
}
|
||||
if c.Gateway.OpenAIProxyStreamCircuit.FailureThreshold < 0 {
|
||||
return fmt.Errorf("gateway.openai_proxy_stream_circuit.failure_threshold must be non-negative")
|
||||
}
|
||||
if c.Gateway.OpenAIProxyStreamCircuit.WindowSeconds < 0 {
|
||||
return fmt.Errorf("gateway.openai_proxy_stream_circuit.window_seconds must be non-negative")
|
||||
}
|
||||
if c.Gateway.OpenAIProxyStreamCircuit.TTLSeconds < 0 {
|
||||
return fmt.Errorf("gateway.openai_proxy_stream_circuit.ttl_seconds must be non-negative")
|
||||
}
|
||||
weights := c.Gateway.OpenAIWS.SchedulerScoreWeights
|
||||
for _, weight := range []float64{
|
||||
weights.Priority, weights.Load, weights.Queue, weights.ErrorRate, weights.TTFT,
|
||||
@@ -3450,14 +3572,12 @@ func generateJWTSecret(byteLength int) (string, error) {
|
||||
// GetServerAddress returns the server address (host:port) from config file or environment variable.
|
||||
// This is a lightweight function that can be used before full config validation,
|
||||
// such as during setup wizard startup.
|
||||
// Priority: config.yaml > environment variables > defaults
|
||||
// Priority: environment variables > config file > defaults.
|
||||
func GetServerAddress() string {
|
||||
v := viper.New()
|
||||
v.SetConfigName("config")
|
||||
v.SetConfigType("yaml")
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath("./config")
|
||||
v.AddConfigPath("/etc/sub2api")
|
||||
configureConfigSource(v.SetConfigFile, v.AddConfigPath)
|
||||
|
||||
// Support SERVER_HOST and SERVER_PORT environment variables
|
||||
v.AutomaticEnv()
|
||||
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
func resetViperWithJWTSecret(t *testing.T) {
|
||||
t.Helper()
|
||||
viper.Reset()
|
||||
t.Cleanup(viper.Reset)
|
||||
t.Setenv("CONFIG_FILE", "")
|
||||
t.Setenv("DATA_DIR", "")
|
||||
t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
|
||||
}
|
||||
|
||||
@@ -37,6 +40,15 @@ func TestLoadServerTimingConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadRedisUsernameFromEnvironment(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("REDIS_USERNAME", "app-user")
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "app-user", cfg.Redis.Username)
|
||||
}
|
||||
|
||||
func TestLoadHTTPIngressSafetyDefaults(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
cfg, err := Load()
|
||||
@@ -244,8 +256,53 @@ func TestLoadTrustedProxiesPresenceFromYAML(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTrustedProxiesFromConfigFile(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
configFile := filepath.Join(t.TempDir(), "reporter-config.yaml")
|
||||
require.NoError(t, os.WriteFile(configFile, []byte(`server:
|
||||
host: 192.0.2.10
|
||||
trusted_proxies:
|
||||
- 127.0.0.1/32
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
`), 0o600))
|
||||
t.Setenv("CONFIG_FILE", configFile)
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "192.0.2.10", cfg.Server.Host)
|
||||
require.Equal(t, []string{"127.0.0.1/32", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}, cfg.Server.TrustedProxies)
|
||||
require.True(t, cfg.Server.TrustedProxiesConfigured)
|
||||
}
|
||||
|
||||
func TestConfigFileTakesPrecedenceOverDataDir(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
dataDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte("server:\n host: 192.0.2.20\n"), 0o600))
|
||||
configFile := filepath.Join(t.TempDir(), "explicit.yaml")
|
||||
require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.30\n"), 0o600))
|
||||
t.Setenv("DATA_DIR", dataDir)
|
||||
t.Setenv("CONFIG_FILE", configFile)
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "192.0.2.30", cfg.Server.Host)
|
||||
}
|
||||
|
||||
func TestLoadReturnsErrorForMissingConfigFile(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("CONFIG_FILE", filepath.Join(t.TempDir(), "missing.yaml"))
|
||||
|
||||
_, err := Load()
|
||||
require.ErrorContains(t, err, "read config error")
|
||||
}
|
||||
|
||||
func TestLoadForBootstrapAllowsMissingJWTSecret(t *testing.T) {
|
||||
viper.Reset()
|
||||
t.Cleanup(viper.Reset)
|
||||
t.Setenv("CONFIG_FILE", "")
|
||||
t.Setenv("DATA_DIR", "")
|
||||
t.Setenv("JWT_SECRET", "")
|
||||
|
||||
cfg, err := LoadForBootstrap()
|
||||
@@ -488,6 +545,25 @@ func TestLoadDefaultOpenAIHTTP2Enabled(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, cfg.Gateway.OpenAIHTTP2.Enabled)
|
||||
require.True(t, cfg.Gateway.OpenAIHTTP2.AllowProxyFallbackToHTTP1)
|
||||
require.False(t, cfg.Gateway.OpenAIProxyStreamCircuit.Disabled)
|
||||
require.Equal(t, 2, cfg.Gateway.OpenAIProxyStreamCircuit.FailureThreshold)
|
||||
require.Equal(t, 60, cfg.Gateway.OpenAIProxyStreamCircuit.WindowSeconds)
|
||||
require.Equal(t, 600, cfg.Gateway.OpenAIProxyStreamCircuit.TTLSeconds)
|
||||
}
|
||||
|
||||
func TestLoadOpenAIProxyStreamCircuitFromEnv(t *testing.T) {
|
||||
resetViperWithJWTSecret(t)
|
||||
t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_DISABLED", "true")
|
||||
t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_FAILURE_THRESHOLD", "3")
|
||||
t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_WINDOW_SECONDS", "90")
|
||||
t.Setenv("GATEWAY_OPENAI_PROXY_STREAM_CIRCUIT_TTL_SECONDS", "420")
|
||||
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
require.True(t, cfg.Gateway.OpenAIProxyStreamCircuit.Disabled)
|
||||
require.Equal(t, 3, cfg.Gateway.OpenAIProxyStreamCircuit.FailureThreshold)
|
||||
require.Equal(t, 90, cfg.Gateway.OpenAIProxyStreamCircuit.WindowSeconds)
|
||||
require.Equal(t, 420, cfg.Gateway.OpenAIProxyStreamCircuit.TTLSeconds)
|
||||
}
|
||||
|
||||
func TestLoadOpenAIHTTP2DisabledFromEnv(t *testing.T) {
|
||||
@@ -682,6 +758,21 @@ func TestLoadDefaultSecurityToggles(t *testing.T) {
|
||||
if !cfg.Security.ResponseHeaders.Enabled {
|
||||
t.Fatalf("ResponseHeaders.Enabled = false, want true")
|
||||
}
|
||||
|
||||
wantHosts := []string{
|
||||
"api.kimi.com",
|
||||
"api.moonshot.ai",
|
||||
"api.moonshot.cn",
|
||||
}
|
||||
hostSet := make(map[string]struct{}, len(cfg.Security.URLAllowlist.UpstreamHosts))
|
||||
for _, h := range cfg.Security.URLAllowlist.UpstreamHosts {
|
||||
hostSet[h] = struct{}{}
|
||||
}
|
||||
for _, want := range wantHosts {
|
||||
if _, ok := hostSet[want]; !ok {
|
||||
t.Fatalf("URLAllowlist.UpstreamHosts missing %q; got %v", want, cfg.Security.URLAllowlist.UpstreamHosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaultServerMode(t *testing.T) {
|
||||
@@ -1130,6 +1221,8 @@ func TestNormalizeStringSlice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetServerAddressFromEnv(t *testing.T) {
|
||||
t.Setenv("CONFIG_FILE", "")
|
||||
t.Setenv("DATA_DIR", "")
|
||||
t.Setenv("SERVER_HOST", "127.0.0.1")
|
||||
t.Setenv("SERVER_PORT", "9090")
|
||||
|
||||
@@ -1139,6 +1232,17 @@ func TestGetServerAddressFromEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetServerAddressFromConfigFile(t *testing.T) {
|
||||
configFile := filepath.Join(t.TempDir(), "setup.yaml")
|
||||
require.NoError(t, os.WriteFile(configFile, []byte("server:\n host: 192.0.2.40\n port: 9191\n"), 0o600))
|
||||
t.Setenv("CONFIG_FILE", configFile)
|
||||
t.Setenv("DATA_DIR", "")
|
||||
t.Setenv("SERVER_HOST", "")
|
||||
t.Setenv("SERVER_PORT", "")
|
||||
|
||||
require.Equal(t, "192.0.2.40:9191", GetServerAddress())
|
||||
}
|
||||
|
||||
func TestValidateAbsoluteHTTPURL(t *testing.T) {
|
||||
if err := ValidateAbsoluteHTTPURL("https://example.com/path"); err != nil {
|
||||
t.Fatalf("ValidateAbsoluteHTTPURL valid url error: %v", err)
|
||||
@@ -1735,6 +1839,21 @@ func TestValidateConfigErrors(t *testing.T) {
|
||||
mutate: func(c *Config) { c.Gateway.OpenAIHTTP2.FallbackTTLSeconds = -1 },
|
||||
wantErr: "gateway.openai_http2.fallback_ttl_seconds",
|
||||
},
|
||||
{
|
||||
name: "gateway openai proxy stream circuit threshold",
|
||||
mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.FailureThreshold = -1 },
|
||||
wantErr: "gateway.openai_proxy_stream_circuit.failure_threshold",
|
||||
},
|
||||
{
|
||||
name: "gateway openai proxy stream circuit window",
|
||||
mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.WindowSeconds = -1 },
|
||||
wantErr: "gateway.openai_proxy_stream_circuit.window_seconds",
|
||||
},
|
||||
{
|
||||
name: "gateway openai proxy stream circuit ttl",
|
||||
mutate: func(c *Config) { c.Gateway.OpenAIProxyStreamCircuit.TTLSeconds = -1 },
|
||||
wantErr: "gateway.openai_proxy_stream_circuit.ttl_seconds",
|
||||
},
|
||||
{
|
||||
name: "gateway stream data interval range",
|
||||
mutate: func(c *Config) { c.Gateway.StreamDataIntervalTimeout = 5 },
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateWebAuthnConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configure func(*Config)
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "valid production origin",
|
||||
configure: func(cfg *Config) {
|
||||
cfg.WebAuthn = WebAuthnConfig{
|
||||
Enabled: true,
|
||||
RPDisplayName: "Sub2API",
|
||||
RPID: "sub2api.example.com",
|
||||
RPOrigins: []string{"https://sub2api.example.com"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid localhost development origin",
|
||||
configure: func(cfg *Config) {
|
||||
cfg.WebAuthn = WebAuthnConfig{
|
||||
Enabled: true,
|
||||
RPDisplayName: "Sub2API Dev",
|
||||
RPID: "localhost",
|
||||
RPOrigins: []string{"http://localhost:5173"},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing relying party id",
|
||||
configure: func(cfg *Config) {
|
||||
cfg.WebAuthn = WebAuthnConfig{
|
||||
Enabled: true,
|
||||
RPDisplayName: "Sub2API",
|
||||
RPOrigins: []string{"https://sub2api.example.com"},
|
||||
}
|
||||
},
|
||||
wantError: "webauthn.rp_id",
|
||||
},
|
||||
{
|
||||
name: "relying party id contains scheme",
|
||||
configure: func(cfg *Config) {
|
||||
cfg.WebAuthn = WebAuthnConfig{
|
||||
Enabled: true,
|
||||
RPDisplayName: "Sub2API",
|
||||
RPID: "https://sub2api.example.com",
|
||||
RPOrigins: []string{"https://sub2api.example.com"},
|
||||
}
|
||||
},
|
||||
wantError: "domain without scheme",
|
||||
},
|
||||
{
|
||||
name: "non-local insecure origin",
|
||||
configure: func(cfg *Config) {
|
||||
cfg.WebAuthn = WebAuthnConfig{
|
||||
Enabled: true,
|
||||
RPDisplayName: "Sub2API",
|
||||
RPID: "sub2api.example.com",
|
||||
RPOrigins: []string{"http://sub2api.example.com"},
|
||||
}
|
||||
},
|
||||
wantError: "must use HTTPS",
|
||||
},
|
||||
{
|
||||
name: "origin outside relying party id",
|
||||
configure: func(cfg *Config) {
|
||||
cfg.WebAuthn = WebAuthnConfig{
|
||||
Enabled: true,
|
||||
RPDisplayName: "Sub2API",
|
||||
RPID: "example.com",
|
||||
RPOrigins: []string{"https://example.net"},
|
||||
}
|
||||
},
|
||||
wantError: "not within relying party ID",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
|
||||
cfg, err := Load()
|
||||
require.NoError(t, err)
|
||||
tt.configure(cfg)
|
||||
|
||||
err = cfg.Validate()
|
||||
if tt.wantError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
PlatformGemini = "gemini"
|
||||
PlatformAntigravity = "antigravity"
|
||||
PlatformGrok = "grok"
|
||||
PlatformComposite = "composite"
|
||||
)
|
||||
|
||||
// Account type constants
|
||||
@@ -116,6 +117,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",
|
||||
@@ -132,6 +139,7 @@ var DefaultBedrockModelMapping = map[string]string{
|
||||
// Claude Fable
|
||||
"claude-fable-5": "anthropic.claude-fable-5",
|
||||
// Claude Opus
|
||||
"claude-opus-5": "us.anthropic.claude-opus-5-v1",
|
||||
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
|
||||
"claude-opus-4-7": "us.anthropic.claude-opus-4-7-v1",
|
||||
"claude-opus-4-6-thinking": "us.anthropic.claude-opus-4-6-v1",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package domain
|
||||
|
||||
// ReasoningEffortMapping rewrites one explicit OpenAI/Codex reasoning effort
|
||||
// value to another before the group ceiling is applied.
|
||||
type ReasoningEffortMapping struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
}
|
||||
@@ -111,7 +111,8 @@ type codexJWTOpenAIClaims struct {
|
||||
}
|
||||
|
||||
type codexAccountIndex struct {
|
||||
accountsByKey map[string][]service.Account
|
||||
accountsByKey map[string][]service.Account
|
||||
keysByAccountID map[int64]map[string]struct{}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) ImportCodexSession(c *gin.Context) {
|
||||
@@ -932,7 +933,10 @@ func buildCodexStoredIdentityKeys(accountID, userID, email, accessToken string)
|
||||
}
|
||||
|
||||
func buildCodexAccountIndex(accounts []service.Account) *codexAccountIndex {
|
||||
index := &codexAccountIndex{accountsByKey: map[string][]service.Account{}}
|
||||
index := &codexAccountIndex{
|
||||
accountsByKey: map[string][]service.Account{},
|
||||
keysByAccountID: map[int64]map[string]struct{}{},
|
||||
}
|
||||
for _, account := range accounts {
|
||||
index.Add(account)
|
||||
}
|
||||
@@ -946,40 +950,71 @@ func (i *codexAccountIndex) Add(account service.Account) {
|
||||
if i.accountsByKey == nil {
|
||||
i.accountsByKey = map[string][]service.Account{}
|
||||
}
|
||||
i.remove(account.ID)
|
||||
if i.keysByAccountID == nil {
|
||||
i.keysByAccountID = map[int64]map[string]struct{}{}
|
||||
}
|
||||
keys := buildCodexStoredIdentityKeys(
|
||||
codexCredentialString(account.Credentials, "chatgpt_account_id"),
|
||||
codexCredentialString(account.Credentials, "chatgpt_user_id"),
|
||||
codexCredentialString(account.Credentials, "email"),
|
||||
codexCredentialString(account.Credentials, "access_token"),
|
||||
)
|
||||
orderedKeys := make([]string, 0, len(keys)+1)
|
||||
accountKeys := make(map[string]struct{}, len(keys)+1)
|
||||
for _, key := range keys {
|
||||
i.accountsByKey[key] = upsertCodexAccount(i.accountsByKey[key], account)
|
||||
if _, exists := accountKeys[key]; exists {
|
||||
continue
|
||||
}
|
||||
accountKeys[key] = struct{}{}
|
||||
orderedKeys = append(orderedKeys, key)
|
||||
}
|
||||
if runtimeID := codexCredentialString(account.Credentials, "agent_runtime_id"); runtimeID != "" {
|
||||
key := "agent:" + runtimeID
|
||||
i.accountsByKey[key] = upsertCodexAccount(i.accountsByKey[key], account)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *codexAccountIndex) remove(accountID int64) {
|
||||
for key, accounts := range i.accountsByKey {
|
||||
kept := accounts[:0]
|
||||
for _, account := range accounts {
|
||||
if account.ID != accountID {
|
||||
kept = append(kept, account)
|
||||
}
|
||||
if _, exists := accountKeys[key]; !exists {
|
||||
accountKeys[key] = struct{}{}
|
||||
orderedKeys = append(orderedKeys, key)
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
delete(i.accountsByKey, key)
|
||||
}
|
||||
|
||||
previousKeys := i.keysByAccountID[account.ID]
|
||||
for key := range previousKeys {
|
||||
if _, retained := accountKeys[key]; retained {
|
||||
i.accountsByKey[key] = upsertCodexAccount(i.accountsByKey[key], account)
|
||||
continue
|
||||
}
|
||||
i.accountsByKey[key] = kept
|
||||
i.removeFromKey(key, account.ID)
|
||||
}
|
||||
for _, key := range orderedKeys {
|
||||
if _, retained := previousKeys[key]; retained {
|
||||
continue
|
||||
}
|
||||
i.accountsByKey[key] = append(i.accountsByKey[key], account)
|
||||
}
|
||||
|
||||
if len(accountKeys) > 0 {
|
||||
i.keysByAccountID[account.ID] = accountKeys
|
||||
return
|
||||
}
|
||||
delete(i.keysByAccountID, account.ID)
|
||||
}
|
||||
|
||||
// upsertCodexAccount 保留同一键下的全部候选账号(共享的 account: 键可对应
|
||||
// 团队内多个账号),同一账号重复 Add 时原位替换为最新状态。
|
||||
func (i *codexAccountIndex) removeFromKey(key string, accountID int64) {
|
||||
accounts := i.accountsByKey[key]
|
||||
kept := accounts[:0]
|
||||
for _, account := range accounts {
|
||||
if account.ID != accountID {
|
||||
kept = append(kept, account)
|
||||
}
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
delete(i.accountsByKey, key)
|
||||
return
|
||||
}
|
||||
i.accountsByKey[key] = kept
|
||||
}
|
||||
|
||||
// upsertCodexAccount keeps all candidates for shared keys while replacing an
|
||||
// existing account in place so ambiguous legacy matches retain their order.
|
||||
func upsertCodexAccount(accounts []service.Account, account service.Account) []service.Account {
|
||||
for idx := range accounts {
|
||||
if accounts[idx].ID == account.ID {
|
||||
|
||||
@@ -509,6 +509,73 @@ func TestCodexAccountIndexUpsertReplacesSameAccount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexAccountIndexUpdateRemovesAllPreviousKeys(t *testing.T) {
|
||||
legacy := service.Account{
|
||||
ID: 50,
|
||||
Credentials: map[string]any{
|
||||
"chatgpt_account_id": "team-old",
|
||||
"chatgpt_user_id": "user-old",
|
||||
"email": "old@example.com",
|
||||
"access_token": "access-old",
|
||||
"agent_runtime_id": "runtime-old",
|
||||
},
|
||||
}
|
||||
index := buildCodexAccountIndex([]service.Account{legacy})
|
||||
|
||||
updated := service.Account{
|
||||
ID: 50,
|
||||
Credentials: map[string]any{
|
||||
"chatgpt_account_id": "team-new",
|
||||
"chatgpt_user_id": "user-new",
|
||||
"email": "new@example.com",
|
||||
"access_token": "access-new",
|
||||
"agent_runtime_id": "runtime-new",
|
||||
},
|
||||
}
|
||||
index.Add(updated)
|
||||
|
||||
oldKeys := append(buildCodexStoredIdentityKeys("team-old", "user-old", "old@example.com", "access-old"), "agent:runtime-old")
|
||||
for _, key := range oldKeys {
|
||||
if got, matchedKey := index.Find([]string{key}, "user-old"); got != nil {
|
||||
t.Fatalf("stale account matched by %q: account ID %d", matchedKey, got.ID)
|
||||
}
|
||||
}
|
||||
|
||||
newKeys := append(buildCodexStoredIdentityKeys("team-new", "user-new", "new@example.com", "access-new"), "agent:runtime-new")
|
||||
for _, key := range newKeys {
|
||||
got, matchedKey := index.Find([]string{key}, "user-new")
|
||||
if got == nil || got.ID != updated.ID {
|
||||
t.Fatalf("updated account not found by %q: account=%v matched=%q", key, got, matchedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexAccountIndexUpdatePreservesSharedKeyCandidateOrder(t *testing.T) {
|
||||
first := service.Account{
|
||||
ID: 60,
|
||||
Credentials: map[string]any{
|
||||
"chatgpt_account_id": "team-shared",
|
||||
"access_token": "access-first-old",
|
||||
},
|
||||
}
|
||||
second := service.Account{
|
||||
ID: 61,
|
||||
Credentials: map[string]any{
|
||||
"chatgpt_account_id": "team-shared",
|
||||
"access_token": "access-second",
|
||||
},
|
||||
}
|
||||
index := buildCodexAccountIndex([]service.Account{first, second})
|
||||
|
||||
first.Credentials["access_token"] = "access-first-new"
|
||||
index.Add(first)
|
||||
|
||||
got, matchedKey := index.Find([]string{"account:team-shared"}, "")
|
||||
if got == nil || got.ID != first.ID {
|
||||
t.Fatalf("shared key candidate order changed after update: account=%v matched=%q", got, matchedKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexIdentitySeenDistinguishesTeamMembers(t *testing.T) {
|
||||
seen := map[string]codexSeenIdentity{}
|
||||
member1 := buildCodexImportIdentityKeys("team-1", "user-1", "", "token-1", "refresh-1")
|
||||
|
||||
@@ -63,6 +63,7 @@ type AccountHandler struct {
|
||||
tokenCacheInvalidator service.TokenCacheInvalidator
|
||||
grokImportProber grokImportProber
|
||||
upstreamBillingProbe *service.UpstreamBillingProbeService
|
||||
ollamaCloudUsage *service.OllamaCloudUsageService
|
||||
}
|
||||
|
||||
// SetUpstreamBillingProbeService attaches the optional remote billing probe service.
|
||||
@@ -70,6 +71,10 @@ func (h *AccountHandler) SetUpstreamBillingProbeService(probe *service.UpstreamB
|
||||
h.upstreamBillingProbe = probe
|
||||
}
|
||||
|
||||
func (h *AccountHandler) SetOllamaCloudUsageService(usage *service.OllamaCloudUsageService) {
|
||||
h.ollamaCloudUsage = usage
|
||||
}
|
||||
|
||||
// NewAccountHandler creates a new admin account handler
|
||||
func NewAccountHandler(
|
||||
adminService service.AdminService,
|
||||
@@ -142,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"` // 用户确认混合渠道风险
|
||||
}
|
||||
|
||||
@@ -208,9 +215,17 @@ type AccountSchedulerGroupScore struct {
|
||||
|
||||
const accountListGroupUngroupedQueryValue = "ungrouped"
|
||||
|
||||
func (h *AccountHandler) accountResponseFromService(account *service.Account) *dto.Account {
|
||||
out := dto.AccountFromService(account)
|
||||
if h != nil && h.ollamaCloudUsage != nil && out != nil {
|
||||
h.ollamaCloudUsage.EnrichState(out.OllamaCloudUsage)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, account *service.Account) AccountWithConcurrency {
|
||||
item := AccountWithConcurrency{
|
||||
Account: dto.AccountFromService(account),
|
||||
Account: h.accountResponseFromService(account),
|
||||
CurrentConcurrency: 0,
|
||||
}
|
||||
if account == nil {
|
||||
@@ -523,6 +538,16 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if h.ollamaCloudUsage != nil && len(accounts) > 0 {
|
||||
accountPointers := make([]*service.Account, len(accounts))
|
||||
for index := range accounts {
|
||||
accountPointers[index] = &accounts[index]
|
||||
}
|
||||
if err := h.ollamaCloudUsage.ResolveAccounts(c.Request.Context(), accountPointers); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get current concurrency counts for all accounts
|
||||
accountIDs := make([]int64, len(accounts))
|
||||
@@ -626,7 +651,7 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
for i := range accounts {
|
||||
acc := &accounts[i]
|
||||
item := AccountWithConcurrency{
|
||||
Account: dto.AccountFromService(acc),
|
||||
Account: h.accountResponseFromService(acc),
|
||||
CurrentConcurrency: concurrencyCounts[acc.ID],
|
||||
SchedulerScore: schedulerScores[acc.ID],
|
||||
SchedulerScores: schedulerGroupScores[acc.ID],
|
||||
@@ -740,6 +765,12 @@ func (h *AccountHandler) GetByID(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if h.ollamaCloudUsage != nil {
|
||||
if err := h.ollamaCloudUsage.ResolveAccounts(c.Request.Context(), []*service.Account{account}); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||
}
|
||||
@@ -958,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 {
|
||||
@@ -1502,6 +1535,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) {
|
||||
|
||||
@@ -213,6 +213,38 @@ func TestAccountHandlerGetAvailableModels_OpenAIOAuthPassthroughFallsBackToDefau
|
||||
require.NotEqual(t, "gpt-5", resp.Data[0].ID)
|
||||
}
|
||||
|
||||
func TestAccountHandlerGetAvailableModels_OpenAIAPIKeyDefaultsToConcreteGPT56Sol(t *testing.T) {
|
||||
svc := &availableModelsAdminService{
|
||||
stubAdminService: newStubAdminService(),
|
||||
account: service.Account{
|
||||
ID: 46,
|
||||
Name: "openai-apikey",
|
||||
Platform: service.PlatformOpenAI,
|
||||
Type: service.AccountTypeAPIKey,
|
||||
Status: service.StatusActive,
|
||||
Credentials: map[string]any{
|
||||
"api_key": "test-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
router := setupAvailableModelsRouter(svc)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts/46/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))
|
||||
require.NotEmpty(t, resp.Data)
|
||||
require.Equal(t, "gpt-5.6-sol", resp.Data[0].ID)
|
||||
}
|
||||
|
||||
func TestAccountHandlerGetAvailableModels_OpenAISparkShadowReturnsMappingModels(t *testing.T) {
|
||||
parentID := int64(100)
|
||||
svc := &availableModelsAdminService{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ollamaCloudUsageSessionRequest struct {
|
||||
Session string `json:"session" binding:"required"`
|
||||
}
|
||||
|
||||
type ollamaCloudUsageAutoRefreshRequest struct {
|
||||
Enabled *bool `json:"enabled" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *AccountHandler) GetOllamaCloudUsageSettings(c *gin.Context) {
|
||||
if h.ollamaCloudUsage == nil {
|
||||
response.ErrorFrom(c, service.ErrOllamaCloudUsageUnavailable)
|
||||
return
|
||||
}
|
||||
settings, err := h.ollamaCloudUsage.GetSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, settings)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) UpdateOllamaCloudUsageSettings(c *gin.Context) {
|
||||
if h.ollamaCloudUsage == nil {
|
||||
response.ErrorFrom(c, service.ErrOllamaCloudUsageUnavailable)
|
||||
return
|
||||
}
|
||||
var req service.OllamaCloudUsageSettings
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.ollamaCloudUsage.UpdateSettings(c.Request.Context(), &req); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
settings, err := h.ollamaCloudUsage.GetSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, settings)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) GetOllamaCloudUsage(c *gin.Context) {
|
||||
if !h.requireOllamaCloudUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := ollamaCloudUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
state, err := h.ollamaCloudUsage.GetState(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) SaveOllamaCloudUsageSession(c *gin.Context) {
|
||||
if !h.requireOllamaCloudUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := ollamaCloudUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req ollamaCloudUsageSessionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
state, err := h.ollamaCloudUsage.SaveSession(c.Request.Context(), accountID, req.Session)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) DeleteOllamaCloudUsageSession(c *gin.Context) {
|
||||
if !h.requireOllamaCloudUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := ollamaCloudUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
state, err := h.ollamaCloudUsage.DeleteSession(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) SetOllamaCloudUsageAutoRefresh(c *gin.Context) {
|
||||
if !h.requireOllamaCloudUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := ollamaCloudUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req ollamaCloudUsageAutoRefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
state, err := h.ollamaCloudUsage.SetAutoRefresh(c.Request.Context(), accountID, *req.Enabled)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) RefreshOllamaCloudUsage(c *gin.Context) {
|
||||
if !h.requireOllamaCloudUsage(c) {
|
||||
return
|
||||
}
|
||||
accountID, ok := ollamaCloudUsageAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
state, err := h.ollamaCloudUsage.Refresh(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, state)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) requireOllamaCloudUsage(c *gin.Context) bool {
|
||||
if h != nil && h.ollamaCloudUsage != nil {
|
||||
return true
|
||||
}
|
||||
response.ErrorFrom(c, service.ErrOllamaCloudUsageUnavailable)
|
||||
return false
|
||||
}
|
||||
|
||||
func ollamaCloudUsageAccountID(c *gin.Context) (int64, bool) {
|
||||
if c == nil {
|
||||
return 0, false
|
||||
}
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil || accountID <= 0 {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return 0, false
|
||||
}
|
||||
return accountID, true
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type ollamaCloudUsageHandlerTestRepo struct {
|
||||
service.AccountRepository
|
||||
account *service.Account
|
||||
accounts []*service.Account
|
||||
groupResolveCalls int
|
||||
}
|
||||
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) GetByID(_ context.Context, id int64) (*service.Account, error) {
|
||||
if r.account != nil && r.account.ID == id {
|
||||
return r.account, nil
|
||||
}
|
||||
for _, account := range r.accounts {
|
||||
if account.ID == id {
|
||||
return account, nil
|
||||
}
|
||||
}
|
||||
return nil, service.ErrAccountNotFound
|
||||
}
|
||||
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) ListOllamaCloudUsageGroupAccounts(_ context.Context, _ []*service.Account) ([]service.Account, error) {
|
||||
r.groupResolveCalls++
|
||||
result := make([]service.Account, 0, len(r.accounts)+1)
|
||||
if r.account != nil {
|
||||
result = append(result, *r.account)
|
||||
}
|
||||
for _, account := range r.accounts {
|
||||
result = append(result, *account)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) SaveOllamaCloudUsageSession(context.Context, *service.Account, string, bool) error {
|
||||
return nil
|
||||
}
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) DeleteOllamaCloudUsageSession(context.Context, *service.Account) error {
|
||||
return nil
|
||||
}
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) SetOllamaCloudUsageAutoRefresh(context.Context, *service.Account, bool) error {
|
||||
return nil
|
||||
}
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) UpdateOllamaCloudUsageSnapshot(context.Context, *service.Account, *service.OllamaCloudUsageSnapshot) error {
|
||||
return nil
|
||||
}
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) DisableOllamaCloudUsageAutoRefresh(context.Context, *service.Account) error {
|
||||
return nil
|
||||
}
|
||||
func (r *ollamaCloudUsageHandlerTestRepo) ListDueOllamaCloudUsageAccounts(context.Context, time.Time, time.Duration, time.Duration, int) ([]service.Account, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func newOllamaCloudUsageHandlerTestService(t *testing.T) *service.OllamaCloudUsageService {
|
||||
t.Helper()
|
||||
svc := service.NewOllamaCloudUsageService(nil, nil, nil, nil, false)
|
||||
t.Cleanup(svc.Stop)
|
||||
return svc
|
||||
}
|
||||
|
||||
func newOllamaCloudUsageHandlerContext(method, target, body, id string) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(method, target, bytes.NewBufferString(body))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = request
|
||||
if id != "" {
|
||||
ctx.Params = gin.Params{{Key: "id", Value: id}}
|
||||
}
|
||||
return ctx, recorder
|
||||
}
|
||||
|
||||
func TestOllamaCloudUsageHandlersValidateRequestsAndDependencies(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svc := newOllamaCloudUsageHandlerTestService(t)
|
||||
|
||||
t.Run("invalid account id", func(t *testing.T) {
|
||||
ctx, recorder := newOllamaCloudUsageHandlerContext(http.MethodGet, "/admin/accounts/not-an-id/ollama-cloud-usage", "", "not-an-id")
|
||||
(&AccountHandler{ollamaCloudUsage: svc}).GetOllamaCloudUsage(ctx)
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
|
||||
t.Run("empty session", func(t *testing.T) {
|
||||
ctx, recorder := newOllamaCloudUsageHandlerContext(http.MethodPut, "/admin/accounts/7/ollama-cloud-usage/session", `{"session":""}`, "7")
|
||||
(&AccountHandler{ollamaCloudUsage: svc}).SaveOllamaCloudUsageSession(ctx)
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
|
||||
t.Run("missing enabled", func(t *testing.T) {
|
||||
ctx, recorder := newOllamaCloudUsageHandlerContext(http.MethodPut, "/admin/accounts/7/ollama-cloud-usage/auto-refresh", `{}`, "7")
|
||||
(&AccountHandler{ollamaCloudUsage: svc}).SetOllamaCloudUsageAutoRefresh(ctx)
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
|
||||
t.Run("service unavailable", func(t *testing.T) {
|
||||
ctx, recorder := newOllamaCloudUsageHandlerContext(http.MethodGet, "/admin/accounts/7/ollama-cloud-usage", "", "7")
|
||||
(&AccountHandler{}).GetOllamaCloudUsage(ctx)
|
||||
require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), "OLLAMA_CLOUD_USAGE_UNAVAILABLE")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOllamaCloudUsageEncryptionKeyStateConsistentAcrossAccountResponses(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
for _, configured := range []bool{false, true} {
|
||||
t.Run("configured="+strconv.FormatBool(configured), func(t *testing.T) {
|
||||
account := &service.Account{
|
||||
ID: 7,
|
||||
Name: "ollama",
|
||||
Platform: service.PlatformOpenAI,
|
||||
Type: service.AccountTypeAPIKey,
|
||||
Credentials: map[string]any{"base_url": "https://ollama.com", "api_key": "test-key"},
|
||||
Extra: map[string]any{},
|
||||
Status: service.StatusActive,
|
||||
}
|
||||
adminService := newStubAdminService()
|
||||
adminService.accounts = []service.Account{*account}
|
||||
adminService.getAccountResult = account
|
||||
usageService := service.NewOllamaCloudUsageService(
|
||||
&ollamaCloudUsageHandlerTestRepo{account: account}, nil, nil, nil, configured,
|
||||
)
|
||||
t.Cleanup(usageService.Stop)
|
||||
|
||||
handler := NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
handler.SetOllamaCloudUsageService(usageService)
|
||||
router := gin.New()
|
||||
router.GET("/accounts", handler.List)
|
||||
router.GET("/accounts/:id", handler.GetByID)
|
||||
router.GET("/accounts/:id/ollama-cloud-usage", handler.GetOllamaCloudUsage)
|
||||
|
||||
listRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(listRecorder, httptest.NewRequest(http.MethodGet, "/accounts?page=1&page_size=20", nil))
|
||||
require.Equal(t, http.StatusOK, listRecorder.Code)
|
||||
var listPayload struct {
|
||||
Data struct {
|
||||
Items []struct {
|
||||
OllamaCloudUsage *service.OllamaCloudUsageState `json:"ollama_cloud_usage"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(listRecorder.Body.Bytes(), &listPayload))
|
||||
require.Len(t, listPayload.Data.Items, 1)
|
||||
require.NotNil(t, listPayload.Data.Items[0].OllamaCloudUsage)
|
||||
|
||||
detailRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(detailRecorder, httptest.NewRequest(http.MethodGet, "/accounts/7", nil))
|
||||
require.Equal(t, http.StatusOK, detailRecorder.Code)
|
||||
var detailPayload struct {
|
||||
Data struct {
|
||||
OllamaCloudUsage *service.OllamaCloudUsageState `json:"ollama_cloud_usage"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(detailRecorder.Body.Bytes(), &detailPayload))
|
||||
require.NotNil(t, detailPayload.Data.OllamaCloudUsage)
|
||||
|
||||
stateRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(stateRecorder, httptest.NewRequest(http.MethodGet, "/accounts/7/ollama-cloud-usage", nil))
|
||||
require.Equal(t, http.StatusOK, stateRecorder.Code)
|
||||
var statePayload struct {
|
||||
Data service.OllamaCloudUsageState `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(stateRecorder.Body.Bytes(), &statePayload))
|
||||
|
||||
listConfigured := listPayload.Data.Items[0].OllamaCloudUsage.EncryptionKeyConfigured
|
||||
detailConfigured := detailPayload.Data.OllamaCloudUsage.EncryptionKeyConfigured
|
||||
require.Equal(t, configured, listConfigured)
|
||||
require.Equal(t, statePayload.Data.EncryptionKeyConfigured, listConfigured)
|
||||
require.Equal(t, statePayload.Data.EncryptionKeyConfigured, detailConfigured)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaCloudUsageSharedStateMatchesListDetailAndSpecialEndpointWithoutListNPlusOne(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
now := time.Now().UTC()
|
||||
source := &service.Account{
|
||||
ID: 7, Name: "source", Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey,
|
||||
Credentials: map[string]any{"base_url": "https://ollama.com", "api_key": "shared-secret-key"},
|
||||
Extra: map[string]any{
|
||||
service.OllamaCloudUsageSessionExtraKey: "ciphertext-secret",
|
||||
service.OllamaCloudUsageAutoRefreshExtraKey: true,
|
||||
service.OllamaCloudUsageSnapshotExtraKey: &service.OllamaCloudUsageSnapshot{
|
||||
Status: service.OllamaCloudUsageStatusOK, Data: &service.OllamaCloudUsageData{Plan: "pro"},
|
||||
LastAttemptAt: now, NextRefreshAt: now.Add(time.Hour),
|
||||
},
|
||||
},
|
||||
Status: service.StatusActive,
|
||||
}
|
||||
sibling := &service.Account{
|
||||
ID: 8, Name: "sibling", Platform: service.PlatformAnthropic, Type: service.AccountTypeAPIKey,
|
||||
Credentials: map[string]any{"base_url": "HTTPS://WWW.OLLAMA.COM:443/v1", "api_key": "shared-secret-key"},
|
||||
Extra: map[string]any{}, Status: service.StatusActive,
|
||||
}
|
||||
repo := &ollamaCloudUsageHandlerTestRepo{accounts: []*service.Account{source, sibling}}
|
||||
adminService := newStubAdminService()
|
||||
adminService.accounts = []service.Account{*source, *sibling}
|
||||
adminService.getAccountResult = sibling
|
||||
usageService := service.NewOllamaCloudUsageService(repo, nil, nil, nil, true)
|
||||
t.Cleanup(usageService.Stop)
|
||||
handler := NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
handler.SetOllamaCloudUsageService(usageService)
|
||||
router := gin.New()
|
||||
router.GET("/accounts", handler.List)
|
||||
router.GET("/accounts/:id", handler.GetByID)
|
||||
router.GET("/accounts/:id/ollama-cloud-usage", handler.GetOllamaCloudUsage)
|
||||
|
||||
listRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(listRecorder, httptest.NewRequest(http.MethodGet, "/accounts?page=1&page_size=20", nil))
|
||||
require.Equal(t, http.StatusOK, listRecorder.Code)
|
||||
require.Equal(t, 1, repo.groupResolveCalls, "the full list page must use one group-resolution batch")
|
||||
var listPayload struct {
|
||||
Data struct {
|
||||
Items []struct {
|
||||
ID int64 `json:"id"`
|
||||
OllamaCloudUsage *service.OllamaCloudUsageState `json:"ollama_cloud_usage"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(listRecorder.Body.Bytes(), &listPayload))
|
||||
require.Len(t, listPayload.Data.Items, 2)
|
||||
for _, item := range listPayload.Data.Items {
|
||||
require.True(t, item.OllamaCloudUsage.Configured)
|
||||
require.Equal(t, "pro", item.OllamaCloudUsage.Snapshot.Data.Plan)
|
||||
}
|
||||
|
||||
detailRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(detailRecorder, httptest.NewRequest(http.MethodGet, "/accounts/8", nil))
|
||||
require.Equal(t, http.StatusOK, detailRecorder.Code)
|
||||
var detailPayload struct {
|
||||
Data struct {
|
||||
OllamaCloudUsage *service.OllamaCloudUsageState `json:"ollama_cloud_usage"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(detailRecorder.Body.Bytes(), &detailPayload))
|
||||
|
||||
stateRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(stateRecorder, httptest.NewRequest(http.MethodGet, "/accounts/8/ollama-cloud-usage", nil))
|
||||
require.Equal(t, http.StatusOK, stateRecorder.Code)
|
||||
var statePayload struct {
|
||||
Data service.OllamaCloudUsageState `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(stateRecorder.Body.Bytes(), &statePayload))
|
||||
require.Equal(t, statePayload.Data.Configured, detailPayload.Data.OllamaCloudUsage.Configured)
|
||||
require.Equal(t, statePayload.Data.Snapshot, detailPayload.Data.OllamaCloudUsage.Snapshot)
|
||||
for _, body := range []string{listRecorder.Body.String(), detailRecorder.Body.String(), stateRecorder.Body.String()} {
|
||||
require.NotContains(t, body, "shared-secret-key")
|
||||
require.NotContains(t, body, "ciphertext-secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOllamaCloudUsageSettingsHandlerSuccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
ctx, recorder := newOllamaCloudUsageHandlerContext(http.MethodGet, "/admin/accounts/ollama-cloud-usage/settings", "", "")
|
||||
handler := &AccountHandler{ollamaCloudUsage: newOllamaCloudUsageHandlerTestService(t)}
|
||||
|
||||
handler.GetOllamaCloudUsageSettings(ctx)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), `"enabled":false`)
|
||||
require.Contains(t, recorder.Body.String(), `"interval_minutes":60`)
|
||||
require.Contains(t, recorder.Body.String(), `"debounce_minutes":1`)
|
||||
}
|
||||
@@ -34,6 +34,11 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
|
||||
router.GET("/api/v1/admin/groups", groupHandler.List)
|
||||
router.GET("/api/v1/admin/groups/all", groupHandler.GetAll)
|
||||
router.GET("/api/v1/admin/groups/:id/models-list-candidates", groupHandler.GetModelsListCandidates)
|
||||
router.GET("/api/v1/admin/groups/:id/composite-routes", groupHandler.ListCompositeRoutes)
|
||||
router.POST("/api/v1/admin/groups/:id/composite-routes", groupHandler.CreateCompositeRoute)
|
||||
router.POST("/api/v1/admin/groups/:id/composite-routes/preview", groupHandler.PreviewCompositeRoute)
|
||||
router.PUT("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.UpdateCompositeRoute)
|
||||
router.DELETE("/api/v1/admin/groups/:id/composite-routes/:route_id", groupHandler.DeleteCompositeRoute)
|
||||
router.GET("/api/v1/admin/groups/:id", groupHandler.GetByID)
|
||||
router.POST("/api/v1/admin/groups", groupHandler.Create)
|
||||
router.PUT("/api/v1/admin/groups/:id", groupHandler.Update)
|
||||
@@ -184,7 +189,54 @@ func TestGroupHandlerEndpoints(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "gpt-5.5")
|
||||
|
||||
body, _ := json.Marshal(map[string]any{"name": "new", "platform": "anthropic", "subscription_type": "standard"})
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/groups/2/composite-routes", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "openrouter/gpt-5")
|
||||
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"public_model": "openrouter/gpt-5",
|
||||
"match_type": "exact",
|
||||
"target_platform": "openai",
|
||||
"upstream_model": "gpt-5",
|
||||
"endpoint": "chat_completions",
|
||||
"enabled": true,
|
||||
})
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusCreated, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "gpt-5")
|
||||
|
||||
body, _ = json.Marshal(map[string]any{
|
||||
"public_model": "openrouter/gpt-5",
|
||||
"target_platform": "openai",
|
||||
"upstream_model": "gpt-5",
|
||||
"endpoint": "responses",
|
||||
"enabled": true,
|
||||
})
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/groups/2/composite-routes/1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
body, _ = json.Marshal(map[string]any{"model": "gpt-5", "endpoint": "responses"})
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups/2/composite-routes/preview", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), `"source":"detector"`)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/groups/2/composite-routes/1", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
body, _ = json.Marshal(map[string]any{"name": "new", "platform": "anthropic", "subscription_type": "standard"})
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/groups", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -32,6 +32,7 @@ type stubAdminService struct {
|
||||
createAccountErr error
|
||||
createSparkShadowErr error
|
||||
updateAccountErr error
|
||||
lastUpdateAccountInput *service.UpdateAccountInput
|
||||
bulkUpdateAccountErr error
|
||||
lastBulkUpdateAccountInput *service.BulkUpdateAccountsInput
|
||||
getAccountResult *service.Account
|
||||
@@ -293,6 +294,64 @@ func (s *stubAdminService) GetGroupModelsListCandidates(ctx context.Context, id
|
||||
return []string{"claude-sonnet-4-6"}, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) ListCompositeRoutes(ctx context.Context, groupID int64) ([]service.CompositeModelRoute, error) {
|
||||
return []service.CompositeModelRoute{
|
||||
{
|
||||
ID: 1,
|
||||
GroupID: groupID,
|
||||
PublicModel: "openrouter/gpt-5",
|
||||
MatchType: service.CompositeRouteMatchExact,
|
||||
TargetPlatform: service.PlatformOpenAI,
|
||||
UpstreamModel: "gpt-5",
|
||||
Endpoint: service.CompositeRouteEndpointAny,
|
||||
Priority: 100,
|
||||
Enabled: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) CreateCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
|
||||
return &service.CompositeModelRoute{
|
||||
ID: 1,
|
||||
GroupID: groupID,
|
||||
PublicModel: input.PublicModel,
|
||||
MatchType: input.MatchType,
|
||||
TargetPlatform: input.TargetPlatform,
|
||||
UpstreamModel: input.UpstreamModel,
|
||||
Endpoint: input.Endpoint,
|
||||
Priority: input.Priority,
|
||||
Enabled: input.Enabled,
|
||||
Notes: input.Notes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) UpdateCompositeRoute(ctx context.Context, groupID, routeID int64, input service.CompositeRouteInput) (*service.CompositeModelRoute, error) {
|
||||
return &service.CompositeModelRoute{
|
||||
ID: routeID,
|
||||
GroupID: groupID,
|
||||
PublicModel: input.PublicModel,
|
||||
MatchType: input.MatchType,
|
||||
TargetPlatform: input.TargetPlatform,
|
||||
UpstreamModel: input.UpstreamModel,
|
||||
Endpoint: input.Endpoint,
|
||||
Priority: input.Priority,
|
||||
Enabled: input.Enabled,
|
||||
Notes: input.Notes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) DeleteCompositeRoute(ctx context.Context, groupID, routeID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) PreviewCompositeRoute(ctx context.Context, groupID int64, input service.CompositeRoutePreviewRequest) (*service.CompositeRouteDecision, error) {
|
||||
decision, err := service.NewCompositeRouteResolver(nil).Resolve(ctx, groupID, input.Model, input.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &decision, nil
|
||||
}
|
||||
|
||||
func (s *stubAdminService) CreateGroup(ctx context.Context, input *service.CreateGroupInput) (*service.Group, error) {
|
||||
group := service.Group{ID: 200, Name: input.Name, Status: service.StatusActive}
|
||||
return &group, nil
|
||||
@@ -443,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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -25,6 +26,16 @@ type GroupHandler struct {
|
||||
groupCapacityService *service.GroupCapacityService
|
||||
}
|
||||
|
||||
// GetLiveCapability 返回当前服务端是否具备生成 Live attestation 的运行环境。
|
||||
func (h *GroupHandler) GetLiveCapability(c *gin.Context) {
|
||||
err := liveattestation.NewProvider().Check(c.Request.Context())
|
||||
result := gin.H{"supported": err == nil}
|
||||
if err != nil {
|
||||
result["reason"] = err.Error()
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
type optionalLimitField struct {
|
||||
set bool
|
||||
value *float64
|
||||
@@ -87,7 +98,7 @@ func NewGroupHandler(adminService service.AdminService, dashboardService *servic
|
||||
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"`
|
||||
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"`
|
||||
@@ -107,6 +118,9 @@ type CreateGroupRequest struct {
|
||||
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"`
|
||||
@@ -125,6 +139,7 @@ type CreateGroupRequest struct {
|
||||
SupportedModelScopes []string `json:"supported_model_scopes"`
|
||||
// OpenAI Messages 调度配置(仅 openai 平台使用)
|
||||
AllowMessagesDispatch bool `json:"allow_messages_dispatch"`
|
||||
AllowLive bool `json:"allow_live"`
|
||||
RequireOAuthOnly bool `json:"require_oauth_only"`
|
||||
RequirePrivacySet bool `json:"require_privacy_set"`
|
||||
DefaultMappedModel string `json:"default_mapped_model"`
|
||||
@@ -132,6 +147,10 @@ type CreateGroupRequest struct {
|
||||
ModelsListConfig service.GroupModelsListConfig `json:"models_list_config"`
|
||||
// 分组 RPM 上限(0 = 不限制)
|
||||
RPMLimit int `json:"rpm_limit"`
|
||||
// OpenAI/Codex 请求推理强度上限,空字符串表示不限制。
|
||||
MaxReasoningEffort string `json:"max_reasoning_effort"`
|
||||
// OpenAI/Codex 推理强度精确映射。
|
||||
ReasoningEffortMappings []service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
|
||||
// 从指定分组复制账号(创建后自动绑定)
|
||||
CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
|
||||
}
|
||||
@@ -140,7 +159,7 @@ type CreateGroupRequest struct {
|
||||
type UpdateGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok"`
|
||||
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"`
|
||||
@@ -161,6 +180,9 @@ type UpdateGroupRequest struct {
|
||||
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"`
|
||||
@@ -179,6 +201,7 @@ type UpdateGroupRequest struct {
|
||||
SupportedModelScopes *[]string `json:"supported_model_scopes"`
|
||||
// OpenAI Messages 调度配置(仅 openai 平台使用)
|
||||
AllowMessagesDispatch *bool `json:"allow_messages_dispatch"`
|
||||
AllowLive *bool `json:"allow_live"`
|
||||
RequireOAuthOnly *bool `json:"require_oauth_only"`
|
||||
RequirePrivacySet *bool `json:"require_privacy_set"`
|
||||
DefaultMappedModel *string `json:"default_mapped_model"`
|
||||
@@ -186,10 +209,30 @@ type UpdateGroupRequest struct {
|
||||
ModelsListConfig *service.GroupModelsListConfig `json:"models_list_config"`
|
||||
// 分组 RPM 上限(0 = 不限制);nil 表示未提供不改动
|
||||
RPMLimit *int `json:"rpm_limit"`
|
||||
// OpenAI/Codex 请求推理强度上限;空字符串清除,nil 不修改。
|
||||
MaxReasoningEffort *string `json:"max_reasoning_effort"`
|
||||
// nil 不修改,空数组清空,非空数组替换。
|
||||
ReasoningEffortMappings *[]service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
|
||||
// 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号)
|
||||
CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
type CompositeRoutePreviewRequest struct {
|
||||
Model string `json:"model" binding:"required"`
|
||||
Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
|
||||
}
|
||||
|
||||
// List handles listing all groups with pagination
|
||||
// GET /api/v1/admin/groups
|
||||
func (h *GroupHandler) List(c *gin.Context) {
|
||||
@@ -225,6 +268,133 @@ func (h *GroupHandler) List(c *gin.Context) {
|
||||
response.Paginated(c, outGroups, total, page, pageSize)
|
||||
}
|
||||
|
||||
// ListCompositeRoutes handles listing composite model routes for one group.
|
||||
// GET /api/v1/admin/groups/:id/composite-routes
|
||||
func (h *GroupHandler) ListCompositeRoutes(c *gin.Context) {
|
||||
groupID, ok := parsePositiveIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
routes, err := h.adminService.ListCompositeRoutes(c.Request.Context(), groupID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, routes)
|
||||
}
|
||||
|
||||
// CreateCompositeRoute handles creating one composite model route.
|
||||
// POST /api/v1/admin/groups/:id/composite-routes
|
||||
func (h *GroupHandler) CreateCompositeRoute(c *gin.Context) {
|
||||
groupID, ok := parsePositiveIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req CompositeRouteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
route, err := h.adminService.CreateCompositeRoute(c.Request.Context(), groupID, compositeRouteRequestToInput(req, true))
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Created(c, route)
|
||||
}
|
||||
|
||||
// UpdateCompositeRoute handles replacing one composite model route.
|
||||
// PUT /api/v1/admin/groups/:id/composite-routes/:route_id
|
||||
func (h *GroupHandler) UpdateCompositeRoute(c *gin.Context) {
|
||||
groupID, ok := parsePositiveIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
routeID, ok := parsePositiveIDParam(c, "route_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req CompositeRouteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
route, err := h.adminService.UpdateCompositeRoute(c.Request.Context(), groupID, routeID, compositeRouteRequestToInput(req, true))
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, route)
|
||||
}
|
||||
|
||||
// DeleteCompositeRoute handles deleting one composite model route.
|
||||
// DELETE /api/v1/admin/groups/:id/composite-routes/:route_id
|
||||
func (h *GroupHandler) DeleteCompositeRoute(c *gin.Context) {
|
||||
groupID, ok := parsePositiveIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
routeID, ok := parsePositiveIDParam(c, "route_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.adminService.DeleteCompositeRoute(c.Request.Context(), groupID, routeID); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"message": "Composite route deleted"})
|
||||
}
|
||||
|
||||
// PreviewCompositeRoute resolves a model without mutating routes.
|
||||
// POST /api/v1/admin/groups/:id/composite-routes/preview
|
||||
func (h *GroupHandler) PreviewCompositeRoute(c *gin.Context) {
|
||||
groupID, ok := parsePositiveIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req CompositeRoutePreviewRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
decision, err := h.adminService.PreviewCompositeRoute(c.Request.Context(), groupID, service.CompositeRoutePreviewRequest{
|
||||
Model: req.Model,
|
||||
Endpoint: req.Endpoint,
|
||||
})
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, decision)
|
||||
}
|
||||
|
||||
func compositeRouteRequestToInput(req CompositeRouteRequest, defaultEnabled bool) service.CompositeRouteInput {
|
||||
enabled := defaultEnabled
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
return service.CompositeRouteInput{
|
||||
PublicModel: req.PublicModel,
|
||||
MatchType: req.MatchType,
|
||||
TargetPlatform: req.TargetPlatform,
|
||||
UpstreamModel: req.UpstreamModel,
|
||||
Endpoint: req.Endpoint,
|
||||
Priority: req.Priority,
|
||||
Enabled: enabled,
|
||||
Notes: req.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
func parsePositiveIDParam(c *gin.Context, name string) (int64, bool) {
|
||||
raw := c.Param(name)
|
||||
id, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
response.BadRequest(c, "Invalid "+name)
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
// GetAll handles getting all active groups without pagination.
|
||||
// Pass ?include_inactive=true to also include disabled groups (used by the
|
||||
// API Key group filter, which needs to surface groups that still have API keys
|
||||
@@ -311,6 +481,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,
|
||||
@@ -333,6 +510,9 @@ 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,
|
||||
@@ -348,12 +528,15 @@ func (h *GroupHandler) Create(c *gin.Context) {
|
||||
MCPXMLInject: req.MCPXMLInject,
|
||||
SupportedModelScopes: req.SupportedModelScopes,
|
||||
AllowMessagesDispatch: req.AllowMessagesDispatch,
|
||||
AllowLive: req.AllowLive,
|
||||
RequireOAuthOnly: req.RequireOAuthOnly,
|
||||
RequirePrivacySet: req.RequirePrivacySet,
|
||||
DefaultMappedModel: req.DefaultMappedModel,
|
||||
MessagesDispatchModelConfig: req.MessagesDispatchModelConfig,
|
||||
ModelsListConfig: req.ModelsListConfig,
|
||||
RPMLimit: req.RPMLimit,
|
||||
MaxReasoningEffort: req.MaxReasoningEffort,
|
||||
ReasoningEffortMappings: req.ReasoningEffortMappings,
|
||||
CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -449,6 +632,9 @@ 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,
|
||||
@@ -464,12 +650,15 @@ func (h *GroupHandler) Update(c *gin.Context) {
|
||||
MCPXMLInject: req.MCPXMLInject,
|
||||
SupportedModelScopes: req.SupportedModelScopes,
|
||||
AllowMessagesDispatch: req.AllowMessagesDispatch,
|
||||
AllowLive: req.AllowLive,
|
||||
RequireOAuthOnly: req.RequireOAuthOnly,
|
||||
RequirePrivacySet: req.RequirePrivacySet,
|
||||
DefaultMappedModel: req.DefaultMappedModel,
|
||||
MessagesDispatchModelConfig: req.MessagesDispatchModelConfig,
|
||||
ModelsListConfig: req.ModelsListConfig,
|
||||
RPMLimit: req.RPMLimit,
|
||||
MaxReasoningEffort: req.MaxReasoningEffort,
|
||||
ReasoningEffortMappings: req.ReasoningEffortMappings,
|
||||
CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateGroupRequestReasoningEffortMappingsTriState(t *testing.T) {
|
||||
t.Run("omitted means unchanged", func(t *testing.T) {
|
||||
var req UpdateGroupRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{}`), &req))
|
||||
require.Nil(t, req.ReasoningEffortMappings)
|
||||
})
|
||||
|
||||
t.Run("empty array means clear", func(t *testing.T) {
|
||||
var req UpdateGroupRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"reasoning_effort_mappings":[]}`), &req))
|
||||
require.NotNil(t, req.ReasoningEffortMappings)
|
||||
require.Empty(t, *req.ReasoningEffortMappings)
|
||||
})
|
||||
|
||||
t.Run("non empty array means replace", func(t *testing.T) {
|
||||
var req UpdateGroupRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"reasoning_effort_mappings":[{"from":"max","to":"xhigh"}]}`), &req))
|
||||
require.NotNil(t, req.ReasoningEffortMappings)
|
||||
require.Len(t, *req.ReasoningEffortMappings, 1)
|
||||
require.Equal(t, "max", (*req.ReasoningEffortMappings)[0].From)
|
||||
require.Equal(t, "xhigh", (*req.ReasoningEffortMappings)[0].To)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,7 +20,61 @@ import (
|
||||
type OpenAIOAuthHandler struct {
|
||||
openaiOAuthService *service.OpenAIOAuthService
|
||||
adminService service.AdminService
|
||||
quotaService *service.OpenAIQuotaService
|
||||
quotaService openAIQuotaService
|
||||
rateLimitService openAIAccountStateRecoverer
|
||||
}
|
||||
|
||||
type openAIQuotaService interface {
|
||||
QueryUsage(ctx context.Context, accountID int64) (*service.OpenAIQuotaUsage, error)
|
||||
CacheResetCreditsSnapshot(ctx context.Context, accountID int64, credits *service.OpenAIRateLimitResetCredits) error
|
||||
ResetCredit(ctx context.Context, accountID int64) (*service.OpenAIQuotaResetResult, error)
|
||||
}
|
||||
|
||||
type openAIAccountStateRecoverer interface {
|
||||
RecoverAccountState(ctx context.Context, accountID int64, options service.AccountRecoveryOptions) (*service.SuccessfulTestRecoveryResult, error)
|
||||
}
|
||||
|
||||
const (
|
||||
openAIQuotaResetWarningCacheRefreshFailed = "reset_credit_cache_refresh_failed"
|
||||
openAIQuotaResetWarningAccountRecoveryFailed = "account_state_recovery_failed"
|
||||
openAIQuotaResetWarningAccountRefreshFailed = "account_state_refresh_failed"
|
||||
)
|
||||
|
||||
// openAIQuotaResetPostProcessTimeout bounds the work performed AFTER the
|
||||
// (non-refundable) reset credit has already been consumed upstream. The whole
|
||||
// request must stay comfortably inside the panel HTTP client timeout, otherwise
|
||||
// the browser aborts a mutation that already succeeded and the operator retries
|
||||
// it — spending a second credit.
|
||||
const openAIQuotaResetPostProcessTimeout = 8 * time.Second
|
||||
|
||||
type openAIQuotaResetResponse struct {
|
||||
service.OpenAIQuotaResetResult
|
||||
Quota *service.OpenAIQuotaUsage `json:"quota,omitempty"`
|
||||
Account *dto.Account `json:"account,omitempty"`
|
||||
CacheRefreshed bool `json:"cache_refreshed"`
|
||||
AccountStateRecovered bool `json:"account_state_recovered"`
|
||||
WarningCode string `json:"warning_code,omitempty"`
|
||||
}
|
||||
|
||||
// openAIQuotaRefreshResponse is the reset-credit-persisting variant of the quota
|
||||
// query. The usage payload is embedded so the shape stays identical to the plain
|
||||
// query; cache_persisted reports whether the snapshot write succeeded, because a
|
||||
// failed display-cache write must never discard a successful upstream read.
|
||||
type openAIQuotaRefreshResponse struct {
|
||||
service.OpenAIQuotaUsage
|
||||
CachePersisted bool `json:"cache_persisted"`
|
||||
}
|
||||
|
||||
// openAIQuotaResetPostProcessContext detaches the post-reset bookkeeping from the
|
||||
// client connection. The credit is already spent at that point, so account-state
|
||||
// recovery must complete even if the operator closes the tab (mirrors
|
||||
// systemUpdateContext, added for the same reason in #4504).
|
||||
func openAIQuotaResetPostProcessContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
base := context.Background()
|
||||
if ctx != nil {
|
||||
base = context.WithoutCancel(ctx)
|
||||
}
|
||||
return context.WithTimeout(base, openAIQuotaResetPostProcessTimeout)
|
||||
}
|
||||
|
||||
func oauthPlatformFromPath(c *gin.Context) string {
|
||||
@@ -29,12 +86,22 @@ func NewOpenAIOAuthHandler(
|
||||
openaiOAuthService *service.OpenAIOAuthService,
|
||||
adminService service.AdminService,
|
||||
quotaService *service.OpenAIQuotaService,
|
||||
rateLimitService *service.RateLimitService,
|
||||
) *OpenAIOAuthHandler {
|
||||
return &OpenAIOAuthHandler{
|
||||
h := &OpenAIOAuthHandler{
|
||||
openaiOAuthService: openaiOAuthService,
|
||||
adminService: adminService,
|
||||
quotaService: quotaService,
|
||||
}
|
||||
// Assign through explicit nil checks: storing a nil *Service in an interface
|
||||
// field yields a non-nil interface, which would silently defeat the
|
||||
// `== nil` capability guards below and panic instead of returning 400.
|
||||
if quotaService != nil {
|
||||
h.quotaService = quotaService
|
||||
}
|
||||
if rateLimitService != nil {
|
||||
h.rateLimitService = rateLimitService
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// OpenAIGenerateAuthURLRequest represents the request for generating OpenAI auth URL
|
||||
@@ -420,6 +487,7 @@ func (h *OpenAIOAuthHandler) QueryQuota(c *gin.Context) {
|
||||
response.BadRequest(c, "openai quota service is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
usage, err := h.quotaService.QueryUsage(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
@@ -428,6 +496,47 @@ func (h *OpenAIOAuthHandler) QueryQuota(c *gin.Context) {
|
||||
response.Success(c, usage)
|
||||
}
|
||||
|
||||
// RefreshQuota queries the rate-limit / quota usage AND persists the reset-credit
|
||||
// snapshot so the card can be rehydrated without an upstream round-trip.
|
||||
// POST /api/v1/admin/openai/accounts/:id/quota/refresh
|
||||
//
|
||||
// It is a POST (not a GET with a side-effect flag) because it writes account
|
||||
// state: the audit middleware only records mutating verbs, so a persisting GET
|
||||
// would mutate the database without an audit trail.
|
||||
func (h *OpenAIOAuthHandler) RefreshQuota(c *gin.Context) {
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return
|
||||
}
|
||||
if h.quotaService == nil {
|
||||
response.BadRequest(c, "openai quota service is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
usage, err := h.quotaService.QueryUsage(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
if usage == nil {
|
||||
response.Error(c, http.StatusInternalServerError, "openai quota query returned an empty result")
|
||||
return
|
||||
}
|
||||
|
||||
refreshResponse := openAIQuotaRefreshResponse{OpenAIQuotaUsage: *usage}
|
||||
// A failed snapshot write leaves the previous cache intact — report it as a
|
||||
// partial success instead of discarding the usage payload we just fetched,
|
||||
// which would leave the card without a credit count at all.
|
||||
if err := h.quotaService.CacheResetCreditsSnapshot(c.Request.Context(), accountID, usage.RateLimitResetCredits); err != nil {
|
||||
slog.Warn("openai_quota_reset_credit_cache_persist_failed", "account_id", accountID, "error", err)
|
||||
response.Success(c, refreshResponse)
|
||||
return
|
||||
}
|
||||
refreshResponse.CachePersisted = true
|
||||
response.Success(c, refreshResponse)
|
||||
}
|
||||
|
||||
// CreateShadowRequest is the request body for CreateShadow.
|
||||
type CreateShadowRequest struct {
|
||||
Name string `json:"name"`
|
||||
@@ -482,5 +591,63 @@ func (h *OpenAIOAuthHandler) ResetQuota(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
if result == nil {
|
||||
response.Error(c, http.StatusInternalServerError, "openai quota reset returned an empty result")
|
||||
return
|
||||
}
|
||||
|
||||
resetResponse := openAIQuotaResetResponse{OpenAIQuotaResetResult: *result}
|
||||
postCtx, cancelPost := openAIQuotaResetPostProcessContext(c.Request.Context())
|
||||
defer cancelPost()
|
||||
|
||||
// Step 1 — unblocking the account is the whole point of consuming a credit
|
||||
// (#3672 / #3740), so it runs FIRST and is never gated on the display cache.
|
||||
// Recovery is DB-only and leaves the manual `schedulable` switch untouched.
|
||||
if h.rateLimitService == nil {
|
||||
resetResponse.WarningCode = openAIQuotaResetWarningAccountRecoveryFailed
|
||||
response.Success(c, resetResponse)
|
||||
return
|
||||
}
|
||||
if _, err := h.rateLimitService.RecoverAccountState(postCtx, accountID, service.AccountRecoveryOptions{
|
||||
InvalidateToken: true,
|
||||
}); err != nil {
|
||||
// Recovery failures are almost always storage-level; the remaining steps
|
||||
// share that dependency, so stop here instead of compounding the failure.
|
||||
slog.Warn("openai_quota_reset_account_recovery_failed", "account_id", accountID, "error", err)
|
||||
resetResponse.WarningCode = openAIQuotaResetWarningAccountRecoveryFailed
|
||||
response.Success(c, resetResponse)
|
||||
return
|
||||
}
|
||||
resetResponse.AccountStateRecovered = true
|
||||
|
||||
// Step 2 — refresh the reset-credit display cache. A failure here is reported
|
||||
// but must not hide the recovered account row produced by step 3.
|
||||
usage, usageErr := h.quotaService.QueryUsage(postCtx, accountID)
|
||||
switch {
|
||||
case usageErr != nil || usage == nil:
|
||||
slog.Warn("openai_quota_reset_cache_refresh_failed", "account_id", accountID, "error", usageErr)
|
||||
resetResponse.WarningCode = openAIQuotaResetWarningCacheRefreshFailed
|
||||
default:
|
||||
if err := h.quotaService.CacheResetCreditsSnapshot(postCtx, accountID, usage.RateLimitResetCredits); err != nil {
|
||||
slog.Warn("openai_quota_reset_cache_refresh_failed", "account_id", accountID, "error", err)
|
||||
resetResponse.WarningCode = openAIQuotaResetWarningCacheRefreshFailed
|
||||
} else {
|
||||
resetResponse.Quota = usage
|
||||
resetResponse.CacheRefreshed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3 — hand back the post-recovery account row so the list drops the
|
||||
// stale rate-limit badge without waiting for the next poll.
|
||||
account, err := h.adminService.GetAccount(postCtx, accountID)
|
||||
if err != nil {
|
||||
slog.Warn("openai_quota_reset_account_refresh_failed", "account_id", accountID, "error", err)
|
||||
if resetResponse.WarningCode == "" {
|
||||
resetResponse.WarningCode = openAIQuotaResetWarningAccountRefreshFailed
|
||||
}
|
||||
response.Success(c, resetResponse)
|
||||
return
|
||||
}
|
||||
resetResponse.Account = dto.AccountFromService(account)
|
||||
response.Success(c, resetResponse)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
//go:build unit
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
type openAIQuotaWorkflowStub struct {
|
||||
resetResult *service.OpenAIQuotaResetResult
|
||||
resetErr error
|
||||
queryResult *service.OpenAIQuotaUsage
|
||||
queryErr error
|
||||
cacheErr error
|
||||
|
||||
resetCalls int
|
||||
queryCalls int
|
||||
cacheCalls int
|
||||
|
||||
queryCtxErr error
|
||||
cacheCtxErr error
|
||||
}
|
||||
|
||||
func (s *openAIQuotaWorkflowStub) ResetCredit(context.Context, int64) (*service.OpenAIQuotaResetResult, error) {
|
||||
s.resetCalls++
|
||||
return s.resetResult, s.resetErr
|
||||
}
|
||||
|
||||
func (s *openAIQuotaWorkflowStub) QueryUsage(ctx context.Context, _ int64) (*service.OpenAIQuotaUsage, error) {
|
||||
s.queryCalls++
|
||||
s.queryCtxErr = ctx.Err()
|
||||
return s.queryResult, s.queryErr
|
||||
}
|
||||
|
||||
func (s *openAIQuotaWorkflowStub) CacheResetCreditsSnapshot(ctx context.Context, _ int64, _ *service.OpenAIRateLimitResetCredits) error {
|
||||
s.cacheCalls++
|
||||
s.cacheCtxErr = ctx.Err()
|
||||
return s.cacheErr
|
||||
}
|
||||
|
||||
type openAIAccountStateRecovererStub struct {
|
||||
err error
|
||||
calls int
|
||||
accountID int64
|
||||
lastOptions service.AccountRecoveryOptions
|
||||
lastCtxErr error
|
||||
}
|
||||
|
||||
func (s *openAIAccountStateRecovererStub) RecoverAccountState(ctx context.Context, accountID int64, options service.AccountRecoveryOptions) (*service.SuccessfulTestRecoveryResult, error) {
|
||||
s.calls++
|
||||
s.accountID = accountID
|
||||
s.lastOptions = options
|
||||
s.lastCtxErr = ctx.Err()
|
||||
return &service.SuccessfulTestRecoveryResult{}, s.err
|
||||
}
|
||||
|
||||
type openAIResetAdminServiceStub struct {
|
||||
service.AdminService
|
||||
account *service.Account
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *openAIResetAdminServiceStub) GetAccount(context.Context, int64) (*service.Account, error) {
|
||||
s.calls++
|
||||
return s.account, s.err
|
||||
}
|
||||
|
||||
type openAIQuotaResetEnvelope struct {
|
||||
Code int `json:"code"`
|
||||
Data openAIQuotaResetResponse `json:"data"`
|
||||
}
|
||||
|
||||
type openAIQuotaRefreshEnvelope struct {
|
||||
Code int `json:"code"`
|
||||
Data openAIQuotaRefreshResponse `json:"data"`
|
||||
}
|
||||
|
||||
func performOpenAIQuotaResetRequest(t *testing.T, handler *OpenAIOAuthHandler) (int, openAIQuotaResetEnvelope) {
|
||||
t.Helper()
|
||||
return performOpenAIQuotaResetRequestWithContext(t, handler, nil)
|
||||
}
|
||||
|
||||
// performOpenAIQuotaResetRequestWithContext drives the reset endpoint, optionally
|
||||
// with an already-canceled request context (client disconnect simulation).
|
||||
func performOpenAIQuotaResetRequestWithContext(t *testing.T, handler *OpenAIOAuthHandler, ctx context.Context) (int, openAIQuotaResetEnvelope) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/openai/accounts/:id/reset-quota", handler.ResetQuota)
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/admin/openai/accounts/42/reset-quota", nil)
|
||||
if ctx != nil {
|
||||
request = request.WithContext(ctx)
|
||||
}
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
var envelope openAIQuotaResetEnvelope
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &envelope))
|
||||
return recorder.Code, envelope
|
||||
}
|
||||
|
||||
func performOpenAIQuotaRefreshRequest(t *testing.T, handler *OpenAIOAuthHandler) (int, openAIQuotaRefreshEnvelope) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/openai/accounts/:id/quota/refresh", handler.RefreshQuota)
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/admin/openai/accounts/42/quota/refresh", nil)
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
var envelope openAIQuotaRefreshEnvelope
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &envelope))
|
||||
return recorder.Code, envelope
|
||||
}
|
||||
|
||||
func successfulOpenAIQuotaWorkflowStub() *openAIQuotaWorkflowStub {
|
||||
return &openAIQuotaWorkflowStub{
|
||||
resetResult: &service.OpenAIQuotaResetResult{
|
||||
Code: "success",
|
||||
WindowsReset: 1,
|
||||
},
|
||||
queryResult: &service.OpenAIQuotaUsage{
|
||||
FetchedAt: 123,
|
||||
RateLimitResetCredits: &service.OpenAIRateLimitResetCredits{
|
||||
AvailableCount: 0,
|
||||
Credits: []service.OpenAIRateLimitResetCreditDetail{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func recoveredAccountStub() *openAIResetAdminServiceStub {
|
||||
return &openAIResetAdminServiceStub{account: &service.Account{
|
||||
ID: 42,
|
||||
Name: "recovered",
|
||||
Platform: service.PlatformOpenAI,
|
||||
Type: service.AccountTypeOAuth,
|
||||
Status: service.StatusActive,
|
||||
Schedulable: false,
|
||||
}}
|
||||
}
|
||||
|
||||
func TestOpenAIResetQuota_ResetFailureStopsWorkflow(t *testing.T) {
|
||||
quota := &openAIQuotaWorkflowStub{resetErr: errors.New("upstream reset failed")}
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: &openAIResetAdminServiceStub{},
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, _ := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, status)
|
||||
require.Equal(t, 1, quota.resetCalls)
|
||||
require.Zero(t, quota.queryCalls)
|
||||
require.Zero(t, quota.cacheCalls)
|
||||
require.Zero(t, recoverer.calls)
|
||||
}
|
||||
|
||||
// Account-state recovery is the reason the credit was spent (#3672 / #3740), so it
|
||||
// must run before — and independently of — the reset-credit display cache.
|
||||
func TestOpenAIResetQuota_RecoversAccountStateBeforeRefreshingCache(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
adminService := recoveredAccountStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Empty(t, envelope.Data.WarningCode)
|
||||
require.True(t, envelope.Data.AccountStateRecovered)
|
||||
require.True(t, envelope.Data.CacheRefreshed)
|
||||
require.NotNil(t, envelope.Data.Quota)
|
||||
require.NotNil(t, envelope.Data.Account)
|
||||
require.Equal(t, int64(42), envelope.Data.Account.ID)
|
||||
require.False(t, envelope.Data.Account.Schedulable, "manual scheduling switch must not be flipped")
|
||||
require.Equal(t, int64(42), recoverer.accountID)
|
||||
require.True(t, recoverer.lastOptions.InvalidateToken)
|
||||
require.Equal(t, 1, quota.resetCalls)
|
||||
require.Equal(t, 1, quota.queryCalls)
|
||||
require.Equal(t, 1, quota.cacheCalls)
|
||||
require.Equal(t, 1, recoverer.calls)
|
||||
require.Equal(t, 1, adminService.calls)
|
||||
}
|
||||
|
||||
func TestOpenAIResetQuota_RecoveryFailureStopsWorkflow(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
recoverer := &openAIAccountStateRecovererStub{err: errors.New("recovery failed")}
|
||||
adminService := recoveredAccountStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, openAIQuotaResetWarningAccountRecoveryFailed, envelope.Data.WarningCode)
|
||||
require.False(t, envelope.Data.AccountStateRecovered)
|
||||
require.False(t, envelope.Data.CacheRefreshed)
|
||||
require.Nil(t, envelope.Data.Quota)
|
||||
require.Nil(t, envelope.Data.Account)
|
||||
require.Equal(t, 1, recoverer.calls)
|
||||
require.Zero(t, quota.queryCalls)
|
||||
require.Zero(t, quota.cacheCalls)
|
||||
require.Zero(t, adminService.calls)
|
||||
}
|
||||
|
||||
func TestOpenAIResetQuota_MissingRecovererReportsRecoveryFailure(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
adminService := recoveredAccountStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, openAIQuotaResetWarningAccountRecoveryFailed, envelope.Data.WarningCode)
|
||||
require.False(t, envelope.Data.AccountStateRecovered)
|
||||
require.Zero(t, quota.queryCalls)
|
||||
require.Zero(t, adminService.calls)
|
||||
}
|
||||
|
||||
// A failed cache refresh must not hide the recovered account row: the operator
|
||||
// still needs the list to drop the stale rate-limit badge.
|
||||
func TestOpenAIResetQuota_QueryFailureStillRecoversAndReturnsAccount(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
quota.queryResult = nil
|
||||
quota.queryErr = errors.New("upstream query failed")
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
adminService := recoveredAccountStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, openAIQuotaResetWarningCacheRefreshFailed, envelope.Data.WarningCode)
|
||||
require.True(t, envelope.Data.AccountStateRecovered)
|
||||
require.False(t, envelope.Data.CacheRefreshed)
|
||||
require.Nil(t, envelope.Data.Quota)
|
||||
require.NotNil(t, envelope.Data.Account)
|
||||
require.Equal(t, 1, quota.queryCalls)
|
||||
require.Zero(t, quota.cacheCalls)
|
||||
require.Equal(t, 1, recoverer.calls)
|
||||
require.Equal(t, 1, adminService.calls)
|
||||
}
|
||||
|
||||
func TestOpenAIResetQuota_CacheFailureStillRecoversAndReturnsAccount(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
quota.cacheErr = errors.New("cache write failed")
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
adminService := recoveredAccountStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, openAIQuotaResetWarningCacheRefreshFailed, envelope.Data.WarningCode)
|
||||
require.True(t, envelope.Data.AccountStateRecovered)
|
||||
require.False(t, envelope.Data.CacheRefreshed)
|
||||
require.Nil(t, envelope.Data.Quota)
|
||||
require.NotNil(t, envelope.Data.Account)
|
||||
require.Equal(t, 1, quota.cacheCalls)
|
||||
require.Equal(t, 1, adminService.calls)
|
||||
}
|
||||
|
||||
func TestOpenAIResetQuota_AccountRefreshFailureReportsRecoveredState(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
adminService := &openAIResetAdminServiceStub{err: errors.New("account refresh failed")}
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, openAIQuotaResetWarningAccountRefreshFailed, envelope.Data.WarningCode)
|
||||
require.True(t, envelope.Data.CacheRefreshed)
|
||||
require.True(t, envelope.Data.AccountStateRecovered)
|
||||
require.NotNil(t, envelope.Data.Quota)
|
||||
require.Nil(t, envelope.Data.Account)
|
||||
require.Equal(t, 1, adminService.calls)
|
||||
}
|
||||
|
||||
// The first (most actionable) failure wins so the UI never downgrades a cache
|
||||
// problem into a cosmetic "could not reload the row" message.
|
||||
func TestOpenAIResetQuota_CacheAndAccountFailureKeepsFirstWarning(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
quota.cacheErr = errors.New("cache write failed")
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
adminService := &openAIResetAdminServiceStub{err: errors.New("account refresh failed")}
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, openAIQuotaResetWarningCacheRefreshFailed, envelope.Data.WarningCode)
|
||||
require.True(t, envelope.Data.AccountStateRecovered)
|
||||
require.Nil(t, envelope.Data.Account)
|
||||
}
|
||||
|
||||
// The credit is non-refundable once consumed, so post-reset bookkeeping must
|
||||
// survive a client disconnect instead of leaving the account rate-limited.
|
||||
func TestOpenAIResetQuota_PostProcessingSurvivesClientCancellation(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
adminService := recoveredAccountStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
status, _ := performOpenAIQuotaResetRequestWithContext(t, handler, ctx)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, 1, recoverer.calls)
|
||||
require.NoError(t, recoverer.lastCtxErr, "recovery must not inherit the canceled client context")
|
||||
require.NoError(t, quota.queryCtxErr)
|
||||
require.NoError(t, quota.cacheCtxErr)
|
||||
require.Equal(t, 1, adminService.calls)
|
||||
}
|
||||
|
||||
func TestOpenAIRefreshQuota_PersistsSnapshot(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: &openAIResetAdminServiceStub{},
|
||||
quotaService: quota,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaRefreshRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.True(t, envelope.Data.CachePersisted)
|
||||
require.Equal(t, int64(123), envelope.Data.FetchedAt)
|
||||
require.Equal(t, 1, quota.queryCalls)
|
||||
require.Equal(t, 1, quota.cacheCalls)
|
||||
require.Zero(t, quota.resetCalls)
|
||||
}
|
||||
|
||||
// A rejected snapshot write must never discard the usage payload: otherwise the
|
||||
// card loses its credit count and the reset button stays disabled forever.
|
||||
func TestOpenAIRefreshQuota_PersistFailureStillReturnsUsage(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
quota.queryResult = &service.OpenAIQuotaUsage{
|
||||
FetchedAt: 456,
|
||||
RateLimitResetCredits: &service.OpenAIRateLimitResetCredits{
|
||||
AvailableCount: 2,
|
||||
},
|
||||
}
|
||||
quota.cacheErr = errors.New("expiration details unavailable")
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: &openAIResetAdminServiceStub{},
|
||||
quotaService: quota,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaRefreshRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.False(t, envelope.Data.CachePersisted)
|
||||
require.Equal(t, int64(456), envelope.Data.FetchedAt)
|
||||
require.NotNil(t, envelope.Data.RateLimitResetCredits)
|
||||
require.Equal(t, 2, envelope.Data.RateLimitResetCredits.AvailableCount)
|
||||
require.Equal(t, 1, quota.cacheCalls)
|
||||
}
|
||||
|
||||
// An empty-but-successful upstream read must not be dereferenced blindly.
|
||||
func TestOpenAIQuotaEmptyUsageIsHandledWithoutPanic(t *testing.T) {
|
||||
t.Run("refresh reports an internal error", func(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
quota.queryResult = nil
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: &openAIResetAdminServiceStub{},
|
||||
quotaService: quota,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaRefreshRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, status)
|
||||
require.False(t, envelope.Data.CachePersisted)
|
||||
require.Zero(t, quota.cacheCalls)
|
||||
})
|
||||
|
||||
t.Run("reset degrades to a cache warning", func(t *testing.T) {
|
||||
quota := successfulOpenAIQuotaWorkflowStub()
|
||||
quota.queryResult = nil
|
||||
recoverer := &openAIAccountStateRecovererStub{}
|
||||
adminService := recoveredAccountStub()
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: adminService,
|
||||
quotaService: quota,
|
||||
rateLimitService: recoverer,
|
||||
}
|
||||
|
||||
status, envelope := performOpenAIQuotaResetRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Equal(t, openAIQuotaResetWarningCacheRefreshFailed, envelope.Data.WarningCode)
|
||||
require.True(t, envelope.Data.AccountStateRecovered)
|
||||
require.NotNil(t, envelope.Data.Account)
|
||||
require.Zero(t, quota.cacheCalls)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenAIRefreshQuota_QueryFailureIsReported(t *testing.T) {
|
||||
quota := &openAIQuotaWorkflowStub{queryErr: errors.New("upstream query failed")}
|
||||
handler := &OpenAIOAuthHandler{
|
||||
adminService: &openAIResetAdminServiceStub{},
|
||||
quotaService: quota,
|
||||
}
|
||||
|
||||
status, _ := performOpenAIQuotaRefreshRequest(t, handler)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, status)
|
||||
require.Equal(t, 1, quota.queryCalls)
|
||||
require.Zero(t, quota.cacheCalls)
|
||||
}
|
||||
|
||||
// Storing a nil *Service in an interface field would make the capability guards
|
||||
// non-nil and panic on the first call; the constructor must keep them nil.
|
||||
func TestNewOpenAIOAuthHandlerKeepsNilQuotaCapabilitiesGuarded(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
handler := NewOpenAIOAuthHandler(nil, newStubAdminService(), nil, nil)
|
||||
require.Nil(t, handler.quotaService)
|
||||
require.Nil(t, handler.rateLimitService)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
router.GET("/openai/accounts/:id/quota", handler.QueryQuota)
|
||||
router.POST("/openai/accounts/:id/quota/refresh", handler.RefreshQuota)
|
||||
router.POST("/openai/accounts/:id/reset-quota", handler.ResetQuota)
|
||||
|
||||
for _, tc := range []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodGet, "/openai/accounts/42/quota"},
|
||||
{http.MethodPost, "/openai/accounts/42/quota/refresh"},
|
||||
{http.MethodPost, "/openai/accounts/42/reset-quota"},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, httptest.NewRequest(tc.method, tc.path, nil))
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code, "%s %s", tc.method, tc.path)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func TestCreateShadow_ReturnsCreatedShadow(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
stub := &stubAdminService{}
|
||||
h := NewOpenAIOAuthHandler(nil, stub, nil)
|
||||
h := NewOpenAIOAuthHandler(nil, stub, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/accounts/:id/shadow", h.CreateShadow)
|
||||
@@ -54,7 +54,7 @@ func TestCreateShadow_ReturnsCreatedShadow(t *testing.T) {
|
||||
func TestCreateShadow_InvalidID(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
h := NewOpenAIOAuthHandler(nil, &stubAdminService{}, nil)
|
||||
h := NewOpenAIOAuthHandler(nil, &stubAdminService{}, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/accounts/:id/shadow", h.CreateShadow)
|
||||
@@ -72,7 +72,7 @@ func TestCreateShadow_ServiceError(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
stub := &stubAdminService{createSparkShadowErr: errors.New("database unavailable")}
|
||||
h := NewOpenAIOAuthHandler(nil, stub, nil)
|
||||
h := NewOpenAIOAuthHandler(nil, stub, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/accounts/:id/shadow", h.CreateShadow)
|
||||
@@ -91,7 +91,7 @@ func TestCreateShadow_ServiceError(t *testing.T) {
|
||||
func TestCreateShadow_BadBody(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
h := NewOpenAIOAuthHandler(nil, &stubAdminService{}, nil)
|
||||
h := NewOpenAIOAuthHandler(nil, &stubAdminService{}, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/accounts/:id/shadow", h.CreateShadow)
|
||||
|
||||
@@ -283,7 +283,68 @@ func (h *PaymentHandler) ListPlans(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, plans)
|
||||
groupInfo := h.configService.GetGroupInfoMap(c.Request.Context(), plans)
|
||||
response.Success(c, adminSubscriptionPlansForResponse(plans, groupInfo))
|
||||
}
|
||||
|
||||
type AdminSubscriptionPlanResult struct {
|
||||
ID int64 `json:"id"`
|
||||
GroupID int64 `json:"group_id"`
|
||||
GroupPlatform string `json:"group_platform,omitempty"`
|
||||
GroupName string `json:"group_name,omitempty"`
|
||||
RateMultiplier float64 `json:"rate_multiplier,omitempty"`
|
||||
DailyLimitUSD *float64 `json:"daily_limit_usd,omitempty"`
|
||||
WeeklyLimitUSD *float64 `json:"weekly_limit_usd,omitempty"`
|
||||
MonthlyLimitUSD *float64 `json:"monthly_limit_usd,omitempty"`
|
||||
ModelScopes []string `json:"supported_model_scopes,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Price float64 `json:"price"`
|
||||
OriginalPrice *float64 `json:"original_price,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
ValidityDays int `json:"validity_days"`
|
||||
ValidityUnit string `json:"validity_unit"`
|
||||
Features string `json:"features"`
|
||||
ProductName string `json:"product_name"`
|
||||
ForSale bool `json:"for_sale"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func adminSubscriptionPlansForResponse(plans []*dbent.SubscriptionPlan, groupInfo map[int64]service.PlanGroupInfo) []AdminSubscriptionPlanResult {
|
||||
result := make([]AdminSubscriptionPlanResult, 0, len(plans))
|
||||
for _, p := range plans {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
gi := groupInfo[p.GroupID]
|
||||
result = append(result, AdminSubscriptionPlanResult{
|
||||
ID: int64(p.ID),
|
||||
GroupID: p.GroupID,
|
||||
GroupPlatform: gi.Platform,
|
||||
GroupName: gi.Name,
|
||||
RateMultiplier: gi.RateMultiplier,
|
||||
DailyLimitUSD: gi.DailyLimitUSD,
|
||||
WeeklyLimitUSD: gi.WeeklyLimitUSD,
|
||||
MonthlyLimitUSD: gi.MonthlyLimitUSD,
|
||||
ModelScopes: gi.ModelScopes,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
Price: p.Price,
|
||||
OriginalPrice: p.OriginalPrice,
|
||||
Currency: p.Currency,
|
||||
ValidityDays: p.ValidityDays,
|
||||
ValidityUnit: p.ValidityUnit,
|
||||
Features: p.Features,
|
||||
ProductName: p.ProductName,
|
||||
ForSale: p.ForSale,
|
||||
SortOrder: p.SortOrder,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CreatePlan creates a new subscription plan.
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
func TestSanitizeAdminPaymentOrderForResponseAddsCurrency(t *testing.T) {
|
||||
@@ -46,3 +47,61 @@ func TestSanitizeAdminPaymentOrderForResponseAddsCurrency(t *testing.T) {
|
||||
t.Fatalf("expected provider_snapshot to be omitted, got %s", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminSubscriptionPlansForResponseIncludesCompositeGroupInfo(t *testing.T) {
|
||||
weekly := 25.0
|
||||
now := time.Now()
|
||||
plans := []*dbent.SubscriptionPlan{
|
||||
{
|
||||
ID: 11,
|
||||
GroupID: 7,
|
||||
Name: "All models",
|
||||
Description: "Composite access",
|
||||
Price: 19.99,
|
||||
Currency: "CNY",
|
||||
ValidityDays: 30,
|
||||
ValidityUnit: "days",
|
||||
Features: "OpenAI\nClaude\nGemini\nGrok",
|
||||
ProductName: "Sub2API",
|
||||
ForSale: true,
|
||||
SortOrder: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
}
|
||||
groupInfo := map[int64]service.PlanGroupInfo{
|
||||
7: {
|
||||
Platform: service.PlatformComposite,
|
||||
Name: "Bucket 2 composite",
|
||||
RateMultiplier: 1.5,
|
||||
WeeklyLimitUSD: &weekly,
|
||||
ModelScopes: []string{"openai", "claude", "gemini", "grok"},
|
||||
},
|
||||
}
|
||||
|
||||
got := adminSubscriptionPlansForResponse(plans, groupInfo)
|
||||
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected one plan, got %d", len(got))
|
||||
}
|
||||
if got[0].GroupPlatform != service.PlatformComposite {
|
||||
t.Fatalf("expected composite group platform, got %q", got[0].GroupPlatform)
|
||||
}
|
||||
if got[0].GroupName != "Bucket 2 composite" {
|
||||
t.Fatalf("expected group name to be included, got %q", got[0].GroupName)
|
||||
}
|
||||
if got[0].WeeklyLimitUSD == nil || *got[0].WeeklyLimitUSD != weekly {
|
||||
t.Fatalf("expected weekly limit to be included, got %#v", got[0].WeeklyLimitUSD)
|
||||
}
|
||||
if strings.Join(got[0].ModelScopes, ",") != "openai,claude,gemini,grok" {
|
||||
t.Fatalf("expected model scopes to be preserved, got %#v", got[0].ModelScopes)
|
||||
}
|
||||
// 投影必须保留 ent 原始响应的全部套餐字段:currency 丢失曾导致编辑保存时
|
||||
// 静默清空套餐货币(PlanEditDialog 回传空串 → SetCurrency(""))。
|
||||
if got[0].Currency != "CNY" {
|
||||
t.Fatalf("expected currency to be preserved, got %q", got[0].Currency)
|
||||
}
|
||||
if !got[0].CreatedAt.Equal(now) || !got[0].UpdatedAt.Equal(now) {
|
||||
t.Fatalf("expected created_at/updated_at to be preserved, got %v / %v", got[0].CreatedAt, got[0].UpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ type SettingHandler struct {
|
||||
settingService *service.SettingService
|
||||
emailService *service.EmailService
|
||||
turnstileService *service.TurnstileService
|
||||
aliyunCaptchaService *service.AliyunCaptchaService
|
||||
opsService *service.OpsService
|
||||
paymentConfigService *service.PaymentConfigService
|
||||
paymentService *service.PaymentService
|
||||
@@ -82,6 +83,12 @@ func (h *SettingHandler) SetNotificationEmailService(notificationEmailService *s
|
||||
h.notificationEmailService = notificationEmailService
|
||||
}
|
||||
|
||||
// SetAliyunCaptchaService attaches the Aliyun captcha credential validator without
|
||||
// changing the constructor signature used by existing unit tests.
|
||||
func (h *SettingHandler) SetAliyunCaptchaService(aliyunCaptchaService *service.AliyunCaptchaService) {
|
||||
h.aliyunCaptchaService = aliyunCaptchaService
|
||||
}
|
||||
|
||||
// SetStepUpDeps attaches the services backing the step-up switch preconditions
|
||||
// (enable requires the acting admin to have TOTP enabled; disable is itself a
|
||||
// step-up gated operation), without changing the constructor signature used by
|
||||
@@ -123,6 +130,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
if paymentCfg == nil {
|
||||
paymentCfg = &service.PaymentConfig{}
|
||||
}
|
||||
passkeyConfigured, passkeyRPID, passkeyRPOrigins := h.settingService.PasskeyConfiguration()
|
||||
|
||||
payload := dto.SystemSettings{
|
||||
RegistrationEnabled: settings.RegistrationEnabled,
|
||||
@@ -134,6 +142,10 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
InvitationCodeEnabled: settings.InvitationCodeEnabled,
|
||||
TotpEnabled: settings.TotpEnabled,
|
||||
TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
|
||||
PasskeyEnabled: settings.PasskeyEnabled,
|
||||
PasskeyConfigured: passkeyConfigured,
|
||||
PasskeyRPID: passkeyRPID,
|
||||
PasskeyRPOrigins: passkeyRPOrigins,
|
||||
SessionBindingEnabled: settings.SessionBindingEnabled,
|
||||
StepUpEnabled: settings.StepUpEnabled,
|
||||
AuditLogRetentionDays: settings.AuditLogRetentionDays,
|
||||
@@ -151,6 +163,18 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
TurnstileEnabled: settings.TurnstileEnabled,
|
||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
|
||||
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
||||
TencentCaptchaAppSecretKeyConfigured: settings.TencentCaptchaAppSecretKeyConfigured,
|
||||
TencentCaptchaCloudSecretIDConfigured: settings.TencentCaptchaCloudSecretIDConfigured,
|
||||
TencentCaptchaCloudSecretKeyConfigured: settings.TencentCaptchaCloudSecretKeyConfigured,
|
||||
TencentCaptchaRegion: settings.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: settings.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecretConfigured: settings.AliyunCaptchaAccessKeySecretConfigured,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
APIKeyACLTrustForwardedIP: settings.APIKeyACLTrustForwardedIP,
|
||||
ForwardedClientIPHeaders: settings.ForwardedClientIPHeaders,
|
||||
LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
|
||||
@@ -228,6 +252,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
ContactInfo: settings.ContactInfo,
|
||||
DocURL: settings.DocURL,
|
||||
HomeContent: settings.HomeContent,
|
||||
CompactHomeEnabled: settings.CompactHomeEnabled,
|
||||
HideCcsImportButton: settings.HideCcsImportButton,
|
||||
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
@@ -273,6 +298,9 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
EnableClientDatelineNormalization: settings.EnableClientDatelineNormalization,
|
||||
AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
|
||||
OpenAICodexUserAgent: settings.OpenAICodexUserAgent,
|
||||
OpenAICodexClientVersion: settings.OpenAICodexClientVersion,
|
||||
OpenAICodexClientVersionSynced: settings.OpenAICodexClientVersionSynced,
|
||||
OpenAICodexVersionAutoSyncEnabled: settings.OpenAICodexVersionAutoSyncEnabled,
|
||||
MinCodexVersion: settings.MinCodexVersion,
|
||||
MaxCodexVersion: settings.MaxCodexVersion,
|
||||
CodexCLIOnlyBlacklist: settings.CodexCLIOnlyBlacklist,
|
||||
@@ -339,12 +367,17 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit,
|
||||
PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode,
|
||||
PaymentAlipayForceQRCode: paymentCfg.AlipayForceQRCode,
|
||||
PaymentAlipayMobilePrecreateDeepLink: paymentCfg.AlipayMobilePrecreateDeepLink,
|
||||
|
||||
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
||||
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
||||
|
||||
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
|
||||
|
||||
ModelPlazaEnabled: settings.ModelPlazaEnabled,
|
||||
ModelPlazaRequireAuth: settings.ModelPlazaRequireAuth,
|
||||
ModelPlazaDescription: settings.ModelPlazaDescription,
|
||||
|
||||
AffiliateEnabled: settings.AffiliateEnabled,
|
||||
|
||||
AllowUserViewErrorRequests: settings.AllowUserViewErrorRequests,
|
||||
|
||||
@@ -56,6 +56,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.TotpEnabled != after.TotpEnabled {
|
||||
changed = append(changed, "totp_enabled")
|
||||
}
|
||||
if before.PasskeyEnabled != after.PasskeyEnabled {
|
||||
changed = append(changed, "passkey_enabled")
|
||||
}
|
||||
if before.SessionBindingEnabled != after.SessionBindingEnabled {
|
||||
changed = append(changed, "session_binding_enabled")
|
||||
}
|
||||
@@ -104,6 +107,42 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if req.TurnstileSecretKey != "" {
|
||||
changed = append(changed, "turnstile_secret_key")
|
||||
}
|
||||
if before.TencentCaptchaEnabled != after.TencentCaptchaEnabled {
|
||||
changed = append(changed, "tencent_captcha_enabled")
|
||||
}
|
||||
if before.TencentCaptchaAppID != after.TencentCaptchaAppID {
|
||||
changed = append(changed, "tencent_captcha_app_id")
|
||||
}
|
||||
if req.TencentCaptchaAppSecretKey != "" {
|
||||
changed = append(changed, "tencent_captcha_app_secret_key")
|
||||
}
|
||||
if req.TencentCaptchaCloudSecretID != "" {
|
||||
changed = append(changed, "tencent_captcha_cloud_secret_id")
|
||||
}
|
||||
if req.TencentCaptchaCloudSecretKey != "" {
|
||||
changed = append(changed, "tencent_captcha_cloud_secret_key")
|
||||
}
|
||||
if before.TencentCaptchaRegion != after.TencentCaptchaRegion {
|
||||
changed = append(changed, "tencent_captcha_region")
|
||||
}
|
||||
if before.AliyunCaptchaEnabled != after.AliyunCaptchaEnabled {
|
||||
changed = append(changed, "aliyun_captcha_enabled")
|
||||
}
|
||||
if before.AliyunCaptchaAccessKeyID != after.AliyunCaptchaAccessKeyID {
|
||||
changed = append(changed, "aliyun_captcha_access_key_id")
|
||||
}
|
||||
if req.AliyunCaptchaAccessKeySecret != "" {
|
||||
changed = append(changed, "aliyun_captcha_access_key_secret")
|
||||
}
|
||||
if before.AliyunCaptchaSceneID != after.AliyunCaptchaSceneID {
|
||||
changed = append(changed, "aliyun_captcha_scene_id")
|
||||
}
|
||||
if before.AliyunCaptchaPrefix != after.AliyunCaptchaPrefix {
|
||||
changed = append(changed, "aliyun_captcha_prefix")
|
||||
}
|
||||
if before.AliyunCaptchaRegion != after.AliyunCaptchaRegion {
|
||||
changed = append(changed, "aliyun_captcha_region")
|
||||
}
|
||||
if before.APIKeyACLTrustForwardedIP != after.APIKeyACLTrustForwardedIP {
|
||||
changed = append(changed, "api_key_acl_trust_forwarded_ip")
|
||||
}
|
||||
@@ -296,6 +335,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.HomeContent != after.HomeContent {
|
||||
changed = append(changed, "home_content")
|
||||
}
|
||||
if before.CompactHomeEnabled != after.CompactHomeEnabled {
|
||||
changed = append(changed, "compact_home_enabled")
|
||||
}
|
||||
if before.HideCcsImportButton != after.HideCcsImportButton {
|
||||
changed = append(changed, "hide_ccs_import_button")
|
||||
}
|
||||
@@ -437,6 +479,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.OpenAICodexUserAgent != after.OpenAICodexUserAgent {
|
||||
changed = append(changed, "openai_codex_user_agent")
|
||||
}
|
||||
if before.OpenAICodexClientVersion != after.OpenAICodexClientVersion {
|
||||
changed = append(changed, "openai_codex_client_version")
|
||||
}
|
||||
if before.OpenAICodexVersionAutoSyncEnabled != after.OpenAICodexVersionAutoSyncEnabled {
|
||||
changed = append(changed, "openai_codex_version_auto_sync_enabled")
|
||||
}
|
||||
if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource {
|
||||
changed = append(changed, "payment_visible_method_alipay_source")
|
||||
}
|
||||
@@ -525,6 +573,15 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.AvailableChannelsEnabled != after.AvailableChannelsEnabled {
|
||||
changed = append(changed, "available_channels_enabled")
|
||||
}
|
||||
if before.ModelPlazaEnabled != after.ModelPlazaEnabled {
|
||||
changed = append(changed, "model_plaza_enabled")
|
||||
}
|
||||
if before.ModelPlazaRequireAuth != after.ModelPlazaRequireAuth {
|
||||
changed = append(changed, "model_plaza_require_auth")
|
||||
}
|
||||
if before.ModelPlazaDescription != after.ModelPlazaDescription {
|
||||
changed = append(changed, "model_plaza_description")
|
||||
}
|
||||
if before.AffiliateEnabled != after.AffiliateEnabled {
|
||||
changed = append(changed, "affiliate_enabled")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
//go:build unit
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Saving settings is a whole-document PUT. A client that sends only the field it
|
||||
// cares about must not reset everything else: a payload as small as
|
||||
// `{"risk_control_enabled":true}` used to clear site_name, after which
|
||||
// getStringOrDefault rendered the empty value as the built-in default and the
|
||||
// login page silently changed name.
|
||||
|
||||
func TestUpdateSettingsPartialPayloadKeepsUnsentKeys(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeySiteName: "Example Gateway",
|
||||
service.SettingKeySiteSubtitle: "Example Gateway Platform",
|
||||
service.SettingKeySMTPHost: "smtp.example.com",
|
||||
service.SettingKeySMTPFrom: "noreply@example.com",
|
||||
service.SettingKeyTurnstileEnabled: "true",
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{"risk_control_enabled": true}, nil)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
require.Equal(t, "true", repo.values[service.SettingKeyRiskControlEnabled],
|
||||
"the field the caller actually sent must be written")
|
||||
|
||||
require.Equal(t, "Example Gateway", repo.values[service.SettingKeySiteName])
|
||||
require.Equal(t, "Example Gateway Platform", repo.values[service.SettingKeySiteSubtitle])
|
||||
require.Equal(t, "smtp.example.com", repo.values[service.SettingKeySMTPHost])
|
||||
require.Equal(t, "noreply@example.com", repo.values[service.SettingKeySMTPFrom])
|
||||
require.Equal(t, "true", repo.values[service.SettingKeyTurnstileEnabled])
|
||||
}
|
||||
|
||||
// A full payload keeps whole-document semantics: fields explicitly set to their
|
||||
// zero value are still cleared.
|
||||
func TestUpdateSettingsFullPayloadStillClearsSentEmptyFields(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeySiteName: "Example Gateway",
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{"site_name": ""}, nil)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
require.Equal(t, "", repo.values[service.SettingKeySiteName],
|
||||
"an explicitly sent empty value is a deliberate clear, not an omission")
|
||||
}
|
||||
|
||||
// smtp_from_email is the one request field whose JSON name differs from its
|
||||
// setting key; the alias keeps it from being treated as always-omitted.
|
||||
func TestUpdateSettingsSMTPFromAliasIsWritable(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeySMTPFrom: "old@example.com",
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{"smtp_from_email": "new@example.com"}, nil)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
require.Equal(t, "new@example.com", repo.values[service.SettingKeySMTPFrom])
|
||||
}
|
||||
|
||||
func TestUpdateSettingsRejectsTwoCaptchaProviders(t *testing.T) {
|
||||
h, _ := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeyTurnstileEnabled: "true",
|
||||
service.SettingKeyTurnstileSiteKey: "site-key",
|
||||
service.SettingKeyTurnstileSecretKey: "turnstile-secret",
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{
|
||||
"turnstile_enabled": true,
|
||||
"turnstile_site_key": "site-key",
|
||||
"turnstile_secret_key": "turnstile-secret",
|
||||
"tencent_captcha_enabled": true,
|
||||
"tencent_captcha_app_id": "123456789",
|
||||
"tencent_captcha_app_secret_key": "app-secret",
|
||||
"tencent_captcha_cloud_secret_id": "cloud-secret-id",
|
||||
"tencent_captcha_cloud_secret_key": "cloud-secret-key",
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "cannot be enabled at the same time")
|
||||
}
|
||||
|
||||
func TestUpdateSettingsRequiresFourTencentCaptchaCredentialsWhenEnabled(t *testing.T) {
|
||||
h, _ := newStepUpSwitchTestHandler(t, map[string]string{})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{
|
||||
"tencent_captcha_enabled": true,
|
||||
"tencent_captcha_app_id": "123456789",
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "AppSecretKey")
|
||||
}
|
||||
|
||||
func TestUpdateSettingsRetainsStoredTencentCaptchaCredentialsWhenInputsEmpty(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeyTencentCaptchaAppSecretKey: "stored-app-secret",
|
||||
service.SettingKeyTencentCaptchaCloudSecretID: "stored-cloud-secret-id",
|
||||
service.SettingKeyTencentCaptchaCloudSecretKey: "stored-cloud-secret-key",
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{
|
||||
"tencent_captcha_enabled": true,
|
||||
"tencent_captcha_app_id": "123456789",
|
||||
"tencent_captcha_app_secret_key": "",
|
||||
"tencent_captcha_cloud_secret_id": "",
|
||||
"tencent_captcha_cloud_secret_key": "",
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "stored-app-secret", repo.values[service.SettingKeyTencentCaptchaAppSecretKey])
|
||||
require.Equal(t, "stored-cloud-secret-id", repo.values[service.SettingKeyTencentCaptchaCloudSecretID])
|
||||
require.Equal(t, "stored-cloud-secret-key", repo.values[service.SettingKeyTencentCaptchaCloudSecretKey])
|
||||
}
|
||||
|
||||
// 天御站点决定前端加载哪个 SDK 与服务端打哪个接入点,两端必须一致。
|
||||
// 部分载荷把它重置回中国站,会让已配国际站的部署在下一次任意保存后整体失效。
|
||||
func TestUpdateSettingsPartialPayloadKeepsTencentCaptchaRegion(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeyTencentCaptchaRegion: service.TencentCaptchaRegionINTL,
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{"risk_control_enabled": true}, nil)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, service.TencentCaptchaRegionINTL,
|
||||
repo.values[service.SettingKeyTencentCaptchaRegion])
|
||||
}
|
||||
|
||||
func TestUpdateSettingsNormalizesUnknownTencentCaptchaRegion(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeyTencentCaptchaRegion: service.TencentCaptchaRegionINTL,
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{"tencent_captcha_region": "sgp"}, nil)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, service.TencentCaptchaRegionCN,
|
||||
repo.values[service.SettingKeyTencentCaptchaRegion],
|
||||
"未知站点必须落回中国站,不能写入无法识别的值")
|
||||
}
|
||||
|
||||
func TestUpdateSettingsWritesTencentCaptchaRegionWhenSent(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{"tencent_captcha_region": "intl"}, nil)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, service.TencentCaptchaRegionINTL,
|
||||
repo.values[service.SettingKeyTencentCaptchaRegion])
|
||||
}
|
||||
|
||||
func TestUpdateSettingsValidatesTencentCaptchaAppIDWhenEnabledFlagIsOmitted(t *testing.T) {
|
||||
h, _ := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeyTencentCaptchaEnabled: "true",
|
||||
service.SettingKeyTencentCaptchaAppID: "123456789",
|
||||
service.SettingKeyTencentCaptchaAppSecretKey: "stored-app-secret",
|
||||
service.SettingKeyTencentCaptchaCloudSecretID: "stored-cloud-secret-id",
|
||||
service.SettingKeyTencentCaptchaCloudSecretKey: "stored-cloud-secret-key",
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{
|
||||
"tencent_captcha_app_id": "not-a-number",
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "positive integer")
|
||||
}
|
||||
@@ -64,6 +64,35 @@ func TestDiffSettings_NoChangeWhenEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsAuditRequestDoesNotInheritStoredTencentSecrets(t *testing.T) {
|
||||
req := UpdateSettingsRequest{
|
||||
TencentCaptchaAppSecretKey: " ",
|
||||
TencentCaptchaCloudSecretID: "\t",
|
||||
TencentCaptchaCloudSecretKey: "\n",
|
||||
}
|
||||
|
||||
auditReq := settingsAuditRequest(req)
|
||||
req.TencentCaptchaAppSecretKey = "stored-app-secret"
|
||||
req.TencentCaptchaCloudSecretID = "stored-secret-id"
|
||||
req.TencentCaptchaCloudSecretKey = "stored-secret-key"
|
||||
|
||||
require.Empty(t, auditReq.TencentCaptchaAppSecretKey)
|
||||
require.Empty(t, auditReq.TencentCaptchaCloudSecretID)
|
||||
require.Empty(t, auditReq.TencentCaptchaCloudSecretKey)
|
||||
}
|
||||
|
||||
func TestDiffSettings_DetectsCompactHomeChange(t *testing.T) {
|
||||
changed := diffSettings(
|
||||
&service.SystemSettings{},
|
||||
&service.SystemSettings{CompactHomeEnabled: true},
|
||||
nil,
|
||||
nil,
|
||||
UpdateSettingsRequest{},
|
||||
)
|
||||
|
||||
require.Contains(t, changed, service.SettingKeyCompactHomeEnabled)
|
||||
}
|
||||
|
||||
func TestEqualNullableFloat(t *testing.T) {
|
||||
five := 5.0
|
||||
five2 := 5.0
|
||||
|
||||
@@ -154,6 +154,70 @@ func (h *SettingHandler) UpdateRateLimit429CooldownSettings(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetPanelRateLimitSettings 获取面板 API 限流配置
|
||||
// GET /api/v1/admin/settings/panel-rate-limit
|
||||
func (h *SettingHandler) GetPanelRateLimitSettings(c *gin.Context) {
|
||||
settings, err := h.settingService.GetPanelRateLimitSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.PanelRateLimitSettings{
|
||||
Enabled: settings.Enabled,
|
||||
UserRPM: settings.UserRPM,
|
||||
HeavyRPM: settings.HeavyRPM,
|
||||
ExemptAdmin: settings.ExemptAdmin,
|
||||
PublicIPRPM: settings.PublicIPRPM,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdatePanelRateLimitSettingsRequest 更新面板 API 限流配置请求
|
||||
type UpdatePanelRateLimitSettingsRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
UserRPM int `json:"user_rpm"`
|
||||
HeavyRPM int `json:"heavy_rpm"`
|
||||
ExemptAdmin bool `json:"exempt_admin"`
|
||||
PublicIPRPM int `json:"public_ip_rpm"`
|
||||
}
|
||||
|
||||
// UpdatePanelRateLimitSettings 更新面板 API 限流配置
|
||||
// PUT /api/v1/admin/settings/panel-rate-limit
|
||||
func (h *SettingHandler) UpdatePanelRateLimitSettings(c *gin.Context) {
|
||||
var req UpdatePanelRateLimitSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
settings := &service.PanelRateLimitSettings{
|
||||
Enabled: req.Enabled,
|
||||
UserRPM: req.UserRPM,
|
||||
HeavyRPM: req.HeavyRPM,
|
||||
ExemptAdmin: req.ExemptAdmin,
|
||||
PublicIPRPM: req.PublicIPRPM,
|
||||
}
|
||||
|
||||
if err := h.settingService.SetPanelRateLimitSettings(c.Request.Context(), settings); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updatedSettings, err := h.settingService.GetPanelRateLimitSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.PanelRateLimitSettings{
|
||||
Enabled: updatedSettings.Enabled,
|
||||
UserRPM: updatedSettings.UserRPM,
|
||||
HeavyRPM: updatedSettings.HeavyRPM,
|
||||
ExemptAdmin: updatedSettings.ExemptAdmin,
|
||||
PublicIPRPM: updatedSettings.PublicIPRPM,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStreamTimeoutSettings 获取流超时处理配置
|
||||
// GET /api/v1/admin/settings/stream-timeout
|
||||
func (h *SettingHandler) GetStreamTimeoutSettings(c *gin.Context) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
// UpdateSettingsRequest 更新设置请求
|
||||
@@ -28,6 +31,7 @@ type UpdateSettingsRequest struct {
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
InvitationCodeEnabled bool `json:"invitation_code_enabled"`
|
||||
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
||||
PasskeyEnabled *bool `json:"passkey_enabled"` // Passkey 登录(省略=保持现值)
|
||||
SessionBindingEnabled *bool `json:"session_binding_enabled"` // 会话 IP/UA 绑定(省略=保持现值)
|
||||
StepUpEnabled *bool `json:"step_up_enabled"` // 敏感操作 step-up 2FA(省略=保持现值)
|
||||
AuditLogRetentionDays int `json:"audit_log_retention_days"` // 审计日志保留天数
|
||||
@@ -50,6 +54,22 @@ type UpdateSettingsRequest struct {
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TurnstileSecretKey string `json:"turnstile_secret_key"`
|
||||
|
||||
// 腾讯天御验证码设置
|
||||
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
|
||||
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
|
||||
TencentCaptchaAppSecretKey string `json:"tencent_captcha_app_secret_key"`
|
||||
TencentCaptchaCloudSecretID string `json:"tencent_captcha_cloud_secret_id"`
|
||||
TencentCaptchaCloudSecretKey string `json:"tencent_captcha_cloud_secret_key"`
|
||||
TencentCaptchaRegion string `json:"tencent_captcha_region"`
|
||||
|
||||
// 阿里云验证码 2.0 设置
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaAccessKeyID string `json:"aliyun_captcha_access_key_id"`
|
||||
AliyunCaptchaAccessKeySecret string `json:"aliyun_captcha_access_key_secret"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
|
||||
// API Key IP 访问控制设置
|
||||
APIKeyACLTrustForwardedIP *bool `json:"api_key_acl_trust_forwarded_ip"`
|
||||
ForwardedClientIPHeaders *[]string `json:"forwarded_client_ip_headers"`
|
||||
@@ -139,6 +159,7 @@ type UpdateSettingsRequest struct {
|
||||
ContactInfo string `json:"contact_info"`
|
||||
DocURL string `json:"doc_url"`
|
||||
HomeContent string `json:"home_content"`
|
||||
CompactHomeEnabled bool `json:"compact_home_enabled"`
|
||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||
PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"`
|
||||
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
|
||||
@@ -232,6 +253,8 @@ type UpdateSettingsRequest struct {
|
||||
EnableClientDatelineNormalization *bool `json:"enable_client_dateline_normalization"`
|
||||
AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
|
||||
OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
|
||||
OpenAICodexClientVersion *string `json:"openai_codex_client_version"`
|
||||
OpenAICodexVersionAutoSyncEnabled *bool `json:"openai_codex_version_auto_sync_enabled"`
|
||||
|
||||
// codex_cli_only 加固(global-only)
|
||||
MinCodexVersion string `json:"min_codex_version"`
|
||||
@@ -300,6 +323,8 @@ type UpdateSettingsRequest struct {
|
||||
|
||||
// Force Alipay mobile clients to use QR code payment instead of mobile redirect
|
||||
PaymentAlipayForceQRCode *bool `json:"payment_alipay_force_qrcode"`
|
||||
// Use Alipay face-to-face precreate and an app deep link on mobile clients.
|
||||
PaymentAlipayMobilePrecreateDeepLink *bool `json:"payment_alipay_mobile_precreate_deep_link"`
|
||||
|
||||
// Channel Monitor feature switch
|
||||
ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"`
|
||||
@@ -308,6 +333,11 @@ type UpdateSettingsRequest struct {
|
||||
// Available Channels feature switch (user-facing)
|
||||
AvailableChannelsEnabled *bool `json:"available_channels_enabled"`
|
||||
|
||||
// Model Plaza feature switches + description
|
||||
ModelPlazaEnabled *bool `json:"model_plaza_enabled"`
|
||||
ModelPlazaRequireAuth *bool `json:"model_plaza_require_auth"`
|
||||
ModelPlazaDescription *string `json:"model_plaza_description"`
|
||||
|
||||
// Affiliate (邀请返利) feature switch
|
||||
AffiliateEnabled *bool `json:"affiliate_enabled"`
|
||||
|
||||
@@ -373,12 +403,79 @@ func (h *SettingHandler) ensureActorTotpForStepUp(c *gin.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// settingKeyJSONAliases covers the request fields whose JSON name differs from
|
||||
// the setting key they persist to. Every other field of UpdateSettingsRequest
|
||||
// is named after its setting key.
|
||||
var settingKeyJSONAliases = map[string]string{
|
||||
"smtp_from_email": service.SettingKeySMTPFrom,
|
||||
}
|
||||
|
||||
// settingKeyByJSONName maps the value-typed top-level JSON fields of
|
||||
// UpdateSettingsRequest to the setting key each one writes. Resolved once from
|
||||
// the struct tags so new fields are covered without touching this file.
|
||||
//
|
||||
// Pointer-typed fields are deliberately excluded: they already carry their own
|
||||
// "omitted = keep the stored value" merge in UpdateSettings, and some of them
|
||||
// rely on being rewritten on every save to re-normalize fail-closed security
|
||||
// state (see TestUpdateSettingsMalformedForwardedClientIPHeadersRemainFailClosedWhenOmitted).
|
||||
// Only the value-typed fields are indistinguishable from a deliberate clear.
|
||||
var settingKeyByJSONName = buildSettingKeyByJSONName()
|
||||
|
||||
func buildSettingKeyByJSONName() map[string]string {
|
||||
t := reflect.TypeOf(UpdateSettingsRequest{})
|
||||
out := make(map[string]string, t.NumField())
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.Type.Kind() == reflect.Ptr {
|
||||
continue
|
||||
}
|
||||
name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
||||
if name == "" || name == "-" {
|
||||
continue
|
||||
}
|
||||
if alias, ok := settingKeyJSONAliases[name]; ok {
|
||||
out[name] = alias
|
||||
continue
|
||||
}
|
||||
out[name] = name
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// omittedSettingKeys reports the setting keys this payload never mentioned.
|
||||
// Saving settings is a whole-document PUT, so without this a client that sends
|
||||
// only the one field it cares about resets every other field to a zero value.
|
||||
func omittedSettingKeys(sentFields map[string]json.RawMessage) service.OmittedSettingKeys {
|
||||
omitted := make(service.OmittedSettingKeys, len(settingKeyByJSONName))
|
||||
for jsonName, settingKey := range settingKeyByJSONName {
|
||||
if _, sent := sentFields[jsonName]; !sent {
|
||||
omitted[settingKey] = struct{}{}
|
||||
}
|
||||
}
|
||||
return omitted
|
||||
}
|
||||
|
||||
func settingsAuditRequest(req UpdateSettingsRequest) UpdateSettingsRequest {
|
||||
req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
|
||||
req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
|
||||
req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
|
||||
req.AliyunCaptchaAccessKeySecret = strings.TrimSpace(req.AliyunCaptchaAccessKeySecret)
|
||||
return req
|
||||
}
|
||||
|
||||
func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
var req UpdateSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
var sentFields map[string]json.RawMessage
|
||||
if err := c.ShouldBindBodyWith(&sentFields, binding.JSON); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
var req UpdateSettingsRequest
|
||||
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
auditReq := settingsAuditRequest(req)
|
||||
omitted := omittedSettingKeys(sentFields)
|
||||
|
||||
previousSettings, err := h.settingService.GetAllSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -401,6 +498,17 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
if req.StepUpEnabled != nil {
|
||||
stepUpEnabled = *req.StepUpEnabled
|
||||
}
|
||||
passkeyEnabled := previousSettings.PasskeyEnabled
|
||||
if req.PasskeyEnabled != nil {
|
||||
passkeyEnabled = *req.PasskeyEnabled
|
||||
}
|
||||
if passkeyEnabled {
|
||||
configured, _, _ := h.settingService.PasskeyConfiguration()
|
||||
if !configured {
|
||||
response.BadRequest(c, "Passkey sign-in requires a valid WebAuthn RP ID and allowed HTTPS origins in the deployment configuration")
|
||||
return
|
||||
}
|
||||
}
|
||||
forwardedClientIPHeaders := append([]string(nil), previousSettings.ForwardedClientIPHeaders...)
|
||||
if req.ForwardedClientIPHeaders != nil {
|
||||
forwardedClientIPHeaders = append([]string(nil), (*req.ForwardedClientIPHeaders)...)
|
||||
@@ -483,6 +591,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
|
||||
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
|
||||
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
|
||||
req.TencentCaptchaAppID = strings.TrimSpace(req.TencentCaptchaAppID)
|
||||
req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
|
||||
req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
|
||||
req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
|
||||
if req.SMTPPort <= 0 {
|
||||
req.SMTPPort = 587
|
||||
}
|
||||
@@ -504,6 +616,43 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
req.SMTPUseTLS = previousSettings.SMTPUseTLS
|
||||
}
|
||||
|
||||
turnstileEnabled := req.TurnstileEnabled
|
||||
if _, sent := sentFields["turnstile_enabled"]; !sent {
|
||||
turnstileEnabled = previousSettings.TurnstileEnabled
|
||||
}
|
||||
tencentCaptchaEnabled := req.TencentCaptchaEnabled
|
||||
if _, sent := sentFields["tencent_captcha_enabled"]; !sent {
|
||||
tencentCaptchaEnabled = previousSettings.TencentCaptchaEnabled
|
||||
}
|
||||
aliyunCaptchaEnabled := req.AliyunCaptchaEnabled
|
||||
if _, sent := sentFields["aliyun_captcha_enabled"]; !sent {
|
||||
aliyunCaptchaEnabled = previousSettings.AliyunCaptchaEnabled
|
||||
}
|
||||
enabledCaptchaProviders := 0
|
||||
for _, enabled := range []bool{turnstileEnabled, tencentCaptchaEnabled, aliyunCaptchaEnabled} {
|
||||
if enabled {
|
||||
enabledCaptchaProviders++
|
||||
}
|
||||
}
|
||||
if enabledCaptchaProviders > 1 {
|
||||
response.BadRequest(c, "Multiple captcha providers (Cloudflare Turnstile / Tencent Captcha / Aliyun Captcha) cannot be enabled at the same time")
|
||||
return
|
||||
}
|
||||
// 阿里云地域 normalize:未发送保留已存值,非法值一律按中国内地落库
|
||||
if _, sent := sentFields["aliyun_captcha_region"]; !sent {
|
||||
req.AliyunCaptchaRegion = previousSettings.AliyunCaptchaRegion
|
||||
}
|
||||
if req.AliyunCaptchaRegion != service.AliyunCaptchaRegionSGP {
|
||||
req.AliyunCaptchaRegion = service.AliyunCaptchaRegionCN
|
||||
}
|
||||
// 天御站点 normalize:未发送保留已存值,非法值一律按中国站落库
|
||||
if _, sent := sentFields["tencent_captcha_region"]; !sent {
|
||||
req.TencentCaptchaRegion = previousSettings.TencentCaptchaRegion
|
||||
}
|
||||
if req.TencentCaptchaRegion != service.TencentCaptchaRegionINTL {
|
||||
req.TencentCaptchaRegion = service.TencentCaptchaRegionCN
|
||||
}
|
||||
|
||||
// Turnstile 参数验证
|
||||
if req.TurnstileEnabled {
|
||||
// 检查必填字段
|
||||
@@ -531,6 +680,83 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if tencentCaptchaEnabled {
|
||||
if _, sent := sentFields["tencent_captcha_app_id"]; !sent {
|
||||
req.TencentCaptchaAppID = previousSettings.TencentCaptchaAppID
|
||||
}
|
||||
appID, err := strconv.ParseUint(req.TencentCaptchaAppID, 10, 64)
|
||||
if err != nil || appID == 0 {
|
||||
response.BadRequest(c, "Tencent Captcha CaptchaAppId must be a positive integer when enabled")
|
||||
return
|
||||
}
|
||||
if req.TencentCaptchaAppSecretKey == "" {
|
||||
req.TencentCaptchaAppSecretKey = previousSettings.TencentCaptchaAppSecretKey
|
||||
}
|
||||
if req.TencentCaptchaCloudSecretID == "" {
|
||||
req.TencentCaptchaCloudSecretID = previousSettings.TencentCaptchaCloudSecretID
|
||||
}
|
||||
if req.TencentCaptchaCloudSecretKey == "" {
|
||||
req.TencentCaptchaCloudSecretKey = previousSettings.TencentCaptchaCloudSecretKey
|
||||
}
|
||||
if req.TencentCaptchaAppSecretKey == "" {
|
||||
response.BadRequest(c, "Tencent Captcha AppSecretKey is required when enabled")
|
||||
return
|
||||
}
|
||||
if req.TencentCaptchaCloudSecretID == "" {
|
||||
response.BadRequest(c, "Tencent Cloud SecretId is required when Tencent Captcha is enabled")
|
||||
return
|
||||
}
|
||||
if req.TencentCaptchaCloudSecretKey == "" {
|
||||
response.BadRequest(c, "Tencent Cloud SecretKey is required when Tencent Captcha is enabled")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 阿里云验证码 2.0 参数验证
|
||||
if aliyunCaptchaEnabled {
|
||||
if _, sent := sentFields["aliyun_captcha_scene_id"]; !sent {
|
||||
req.AliyunCaptchaSceneID = previousSettings.AliyunCaptchaSceneID
|
||||
}
|
||||
if _, sent := sentFields["aliyun_captcha_prefix"]; !sent {
|
||||
req.AliyunCaptchaPrefix = previousSettings.AliyunCaptchaPrefix
|
||||
}
|
||||
if _, sent := sentFields["aliyun_captcha_access_key_id"]; !sent {
|
||||
req.AliyunCaptchaAccessKeyID = previousSettings.AliyunCaptchaAccessKeyID
|
||||
}
|
||||
if req.AliyunCaptchaSceneID == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha Scene ID is required when enabled")
|
||||
return
|
||||
}
|
||||
if req.AliyunCaptchaPrefix == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha Prefix is required when enabled")
|
||||
return
|
||||
}
|
||||
if req.AliyunCaptchaAccessKeyID == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha AccessKey ID is required when enabled")
|
||||
return
|
||||
}
|
||||
// 如果未提供 AccessKey Secret,使用已保存的值(留空保留当前值)
|
||||
if req.AliyunCaptchaAccessKeySecret == "" {
|
||||
if previousSettings.AliyunCaptchaAccessKeySecret == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha AccessKey Secret is required when enabled")
|
||||
return
|
||||
}
|
||||
req.AliyunCaptchaAccessKeySecret = previousSettings.AliyunCaptchaAccessKeySecret
|
||||
}
|
||||
|
||||
// 凭证任一变化时真实调用一次阿里云校验(避免配置错误导致无法登录)
|
||||
credentialsChanged := previousSettings.AliyunCaptchaAccessKeyID != req.AliyunCaptchaAccessKeyID ||
|
||||
previousSettings.AliyunCaptchaAccessKeySecret != req.AliyunCaptchaAccessKeySecret ||
|
||||
previousSettings.AliyunCaptchaSceneID != req.AliyunCaptchaSceneID ||
|
||||
previousSettings.AliyunCaptchaRegion != req.AliyunCaptchaRegion
|
||||
if credentialsChanged {
|
||||
if err := h.aliyunCaptchaService.ValidateCredentials(c.Request.Context(), req.AliyunCaptchaAccessKeyID, req.AliyunCaptchaAccessKeySecret, req.AliyunCaptchaSceneID, req.AliyunCaptchaRegion); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TOTP 双因素认证参数验证
|
||||
// 只有手动配置了加密密钥才允许启用 TOTP 功能
|
||||
if req.TotpEnabled && !previousSettings.TotpEnabled {
|
||||
@@ -1196,6 +1422,15 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.OpenAICodexClientVersion != nil {
|
||||
// 该值会被拼进出站 User-Agent 与 version 头,必须是合法版本号;空串表示跟随自动同步。
|
||||
normalized := strings.TrimSpace(*req.OpenAICodexClientVersion)
|
||||
if normalized != "" && service.NormalizeCodexClientVersion(normalized) == "" {
|
||||
response.Error(c, http.StatusBadRequest, "openai_codex_client_version must be empty or a valid version (e.g. 0.146.0)")
|
||||
return
|
||||
}
|
||||
req.OpenAICodexClientVersion = &normalized
|
||||
}
|
||||
|
||||
// codex_cli_only 加固:最低/最高 Codex 版本(空=禁用,或合法 semver;max>=min)
|
||||
if req.MinCodexVersion != "" && !semverPattern.MatchString(req.MinCodexVersion) {
|
||||
@@ -1251,6 +1486,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
FrontendURL: req.FrontendURL,
|
||||
InvitationCodeEnabled: req.InvitationCodeEnabled,
|
||||
TotpEnabled: req.TotpEnabled,
|
||||
PasskeyEnabled: passkeyEnabled,
|
||||
SessionBindingEnabled: sessionBindingEnabled,
|
||||
StepUpEnabled: stepUpEnabled,
|
||||
AuditLogRetentionDays: req.AuditLogRetentionDays,
|
||||
@@ -1268,6 +1504,18 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
TurnstileEnabled: req.TurnstileEnabled,
|
||||
TurnstileSiteKey: req.TurnstileSiteKey,
|
||||
TurnstileSecretKey: req.TurnstileSecretKey,
|
||||
TencentCaptchaEnabled: req.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: req.TencentCaptchaAppID,
|
||||
TencentCaptchaAppSecretKey: req.TencentCaptchaAppSecretKey,
|
||||
TencentCaptchaCloudSecretID: req.TencentCaptchaCloudSecretID,
|
||||
TencentCaptchaCloudSecretKey: req.TencentCaptchaCloudSecretKey,
|
||||
TencentCaptchaRegion: req.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: req.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: req.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecret: req.AliyunCaptchaAccessKeySecret,
|
||||
AliyunCaptchaSceneID: req.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: req.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: req.AliyunCaptchaRegion,
|
||||
APIKeyACLTrustForwardedIP: func() bool {
|
||||
if req.APIKeyACLTrustForwardedIP != nil {
|
||||
return *req.APIKeyACLTrustForwardedIP
|
||||
@@ -1350,6 +1598,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
ContactInfo: req.ContactInfo,
|
||||
DocURL: req.DocURL,
|
||||
HomeContent: req.HomeContent,
|
||||
CompactHomeEnabled: req.CompactHomeEnabled,
|
||||
HideCcsImportButton: req.HideCcsImportButton,
|
||||
PurchaseSubscriptionEnabled: purchaseEnabled,
|
||||
PurchaseSubscriptionURL: purchaseURL,
|
||||
@@ -1473,6 +1722,20 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.OpenAICodexUserAgent
|
||||
}(),
|
||||
OpenAICodexClientVersion: func() string {
|
||||
if req.OpenAICodexClientVersion != nil {
|
||||
return *req.OpenAICodexClientVersion
|
||||
}
|
||||
return previousSettings.OpenAICodexClientVersion
|
||||
}(),
|
||||
// 同步值由自动同步任务独占写入,面板保存时原样带回,避免被清空。
|
||||
OpenAICodexClientVersionSynced: previousSettings.OpenAICodexClientVersionSynced,
|
||||
OpenAICodexVersionAutoSyncEnabled: func() bool {
|
||||
if req.OpenAICodexVersionAutoSyncEnabled != nil {
|
||||
return *req.OpenAICodexVersionAutoSyncEnabled
|
||||
}
|
||||
return previousSettings.OpenAICodexVersionAutoSyncEnabled
|
||||
}(),
|
||||
MinCodexVersion: strings.TrimSpace(req.MinCodexVersion),
|
||||
MaxCodexVersion: strings.TrimSpace(req.MaxCodexVersion),
|
||||
CodexCLIOnlyBlacklist: strings.TrimSpace(req.CodexCLIOnlyBlacklist),
|
||||
@@ -1603,6 +1866,24 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.AvailableChannelsEnabled
|
||||
}(),
|
||||
ModelPlazaEnabled: func() bool {
|
||||
if req.ModelPlazaEnabled != nil {
|
||||
return *req.ModelPlazaEnabled
|
||||
}
|
||||
return previousSettings.ModelPlazaEnabled
|
||||
}(),
|
||||
ModelPlazaRequireAuth: func() bool {
|
||||
if req.ModelPlazaRequireAuth != nil {
|
||||
return *req.ModelPlazaRequireAuth
|
||||
}
|
||||
return previousSettings.ModelPlazaRequireAuth
|
||||
}(),
|
||||
ModelPlazaDescription: func() string {
|
||||
if req.ModelPlazaDescription != nil {
|
||||
return *req.ModelPlazaDescription
|
||||
}
|
||||
return previousSettings.ModelPlazaDescription
|
||||
}(),
|
||||
AffiliateEnabled: func() bool {
|
||||
if req.AffiliateEnabled != nil {
|
||||
return *req.AffiliateEnabled
|
||||
@@ -1690,7 +1971,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
},
|
||||
ForceEmailOnThirdPartySignup: boolValueOrDefault(req.ForceEmailOnThirdPartySignup, previousAuthSourceDefaults.ForceEmailOnThirdPartySignup),
|
||||
}
|
||||
if err := h.settingService.UpdateSettingsWithAuthSourceDefaults(c.Request.Context(), settings, authSourceDefaults); err != nil {
|
||||
if err := h.settingService.UpdateSettingsWithAuthSourceDefaultsOmitting(c.Request.Context(), settings, authSourceDefaults, omitted); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
@@ -1710,28 +1991,29 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
// Skip if no payment fields were provided (prevents accidental wipe).
|
||||
if h.paymentConfigService != nil && hasPaymentFields(req) {
|
||||
paymentReq := service.UpdatePaymentConfigRequest{
|
||||
Enabled: req.PaymentEnabled,
|
||||
MinAmount: req.PaymentMinAmount,
|
||||
MaxAmount: req.PaymentMaxAmount,
|
||||
DailyLimit: req.PaymentDailyLimit,
|
||||
OrderTimeoutMin: req.PaymentOrderTimeoutMin,
|
||||
MaxPendingOrders: req.PaymentMaxPendingOrders,
|
||||
EnabledTypes: req.PaymentEnabledTypes,
|
||||
BalanceDisabled: req.PaymentBalanceDisabled,
|
||||
BalanceRechargeMultiplier: req.PaymentBalanceRechargeMultiplier,
|
||||
SubscriptionUSDToCNYRate: req.PaymentSubscriptionUSDToCNYRate,
|
||||
RechargeFeeRate: req.PaymentRechargeFeeRate,
|
||||
LoadBalanceStrategy: req.PaymentLoadBalanceStrat,
|
||||
ProductNamePrefix: req.PaymentProductNamePrefix,
|
||||
ProductNameSuffix: req.PaymentProductNameSuffix,
|
||||
HelpImageURL: req.PaymentHelpImageURL,
|
||||
HelpText: req.PaymentHelpText,
|
||||
CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled,
|
||||
CancelRateLimitMax: req.PaymentCancelRateLimitMax,
|
||||
CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
|
||||
CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
|
||||
CancelRateLimitMode: req.PaymentCancelRateLimitMode,
|
||||
AlipayForceQRCode: req.PaymentAlipayForceQRCode,
|
||||
Enabled: req.PaymentEnabled,
|
||||
MinAmount: req.PaymentMinAmount,
|
||||
MaxAmount: req.PaymentMaxAmount,
|
||||
DailyLimit: req.PaymentDailyLimit,
|
||||
OrderTimeoutMin: req.PaymentOrderTimeoutMin,
|
||||
MaxPendingOrders: req.PaymentMaxPendingOrders,
|
||||
EnabledTypes: req.PaymentEnabledTypes,
|
||||
BalanceDisabled: req.PaymentBalanceDisabled,
|
||||
BalanceRechargeMultiplier: req.PaymentBalanceRechargeMultiplier,
|
||||
SubscriptionUSDToCNYRate: req.PaymentSubscriptionUSDToCNYRate,
|
||||
RechargeFeeRate: req.PaymentRechargeFeeRate,
|
||||
LoadBalanceStrategy: req.PaymentLoadBalanceStrat,
|
||||
ProductNamePrefix: req.PaymentProductNamePrefix,
|
||||
ProductNameSuffix: req.PaymentProductNameSuffix,
|
||||
HelpImageURL: req.PaymentHelpImageURL,
|
||||
HelpText: req.PaymentHelpText,
|
||||
CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled,
|
||||
CancelRateLimitMax: req.PaymentCancelRateLimitMax,
|
||||
CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
|
||||
CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
|
||||
CancelRateLimitMode: req.PaymentCancelRateLimitMode,
|
||||
AlipayForceQRCode: req.PaymentAlipayForceQRCode,
|
||||
AlipayMobilePrecreateDeepLink: req.PaymentAlipayMobilePrecreateDeepLink,
|
||||
}
|
||||
if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
@@ -1743,7 +2025,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
h.auditSettingsUpdate(c, previousSettings, settings, previousAuthSourceDefaults, authSourceDefaults, req)
|
||||
h.auditSettingsUpdate(c, previousSettings, settings, previousAuthSourceDefaults, authSourceDefaults, auditReq)
|
||||
|
||||
// 重新获取设置返回
|
||||
updatedSettings, err := h.settingService.GetAllSettings(c.Request.Context())
|
||||
@@ -1773,6 +2055,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
if updatedPaymentCfg == nil {
|
||||
updatedPaymentCfg = &service.PaymentConfig{}
|
||||
}
|
||||
passkeyConfigured, passkeyRPID, passkeyRPOrigins := h.settingService.PasskeyConfiguration()
|
||||
|
||||
payload := dto.SystemSettings{
|
||||
RegistrationEnabled: updatedSettings.RegistrationEnabled,
|
||||
@@ -1784,6 +2067,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled,
|
||||
TotpEnabled: updatedSettings.TotpEnabled,
|
||||
TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
|
||||
PasskeyEnabled: updatedSettings.PasskeyEnabled,
|
||||
PasskeyConfigured: passkeyConfigured,
|
||||
PasskeyRPID: passkeyRPID,
|
||||
PasskeyRPOrigins: passkeyRPOrigins,
|
||||
SessionBindingEnabled: updatedSettings.SessionBindingEnabled,
|
||||
StepUpEnabled: updatedSettings.StepUpEnabled,
|
||||
AuditLogRetentionDays: updatedSettings.AuditLogRetentionDays,
|
||||
@@ -1801,6 +2088,18 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
TurnstileEnabled: updatedSettings.TurnstileEnabled,
|
||||
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
|
||||
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
|
||||
TencentCaptchaEnabled: updatedSettings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: updatedSettings.TencentCaptchaAppID,
|
||||
TencentCaptchaAppSecretKeyConfigured: updatedSettings.TencentCaptchaAppSecretKeyConfigured,
|
||||
TencentCaptchaCloudSecretIDConfigured: updatedSettings.TencentCaptchaCloudSecretIDConfigured,
|
||||
TencentCaptchaCloudSecretKeyConfigured: updatedSettings.TencentCaptchaCloudSecretKeyConfigured,
|
||||
TencentCaptchaRegion: updatedSettings.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: updatedSettings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: updatedSettings.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecretConfigured: updatedSettings.AliyunCaptchaAccessKeySecretConfigured,
|
||||
AliyunCaptchaSceneID: updatedSettings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: updatedSettings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: updatedSettings.AliyunCaptchaRegion,
|
||||
APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
|
||||
ForwardedClientIPHeaders: updatedSettings.ForwardedClientIPHeaders,
|
||||
LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
|
||||
@@ -1878,6 +2177,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
ContactInfo: updatedSettings.ContactInfo,
|
||||
DocURL: updatedSettings.DocURL,
|
||||
HomeContent: updatedSettings.HomeContent,
|
||||
CompactHomeEnabled: updatedSettings.CompactHomeEnabled,
|
||||
HideCcsImportButton: updatedSettings.HideCcsImportButton,
|
||||
PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
|
||||
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
|
||||
@@ -1920,6 +2220,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
EnableClientDatelineNormalization: updatedSettings.EnableClientDatelineNormalization,
|
||||
AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
|
||||
OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
|
||||
OpenAICodexClientVersion: updatedSettings.OpenAICodexClientVersion,
|
||||
OpenAICodexClientVersionSynced: updatedSettings.OpenAICodexClientVersionSynced,
|
||||
OpenAICodexVersionAutoSyncEnabled: updatedSettings.OpenAICodexVersionAutoSyncEnabled,
|
||||
MinCodexVersion: updatedSettings.MinCodexVersion,
|
||||
MaxCodexVersion: updatedSettings.MaxCodexVersion,
|
||||
CodexCLIOnlyBlacklist: updatedSettings.CodexCLIOnlyBlacklist,
|
||||
@@ -1985,12 +2288,17 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit,
|
||||
PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode,
|
||||
PaymentAlipayForceQRCode: updatedPaymentCfg.AlipayForceQRCode,
|
||||
PaymentAlipayMobilePrecreateDeepLink: updatedPaymentCfg.AlipayMobilePrecreateDeepLink,
|
||||
|
||||
ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
|
||||
ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
|
||||
|
||||
AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled,
|
||||
|
||||
ModelPlazaEnabled: updatedSettings.ModelPlazaEnabled,
|
||||
ModelPlazaRequireAuth: updatedSettings.ModelPlazaRequireAuth,
|
||||
ModelPlazaDescription: updatedSettings.ModelPlazaDescription,
|
||||
|
||||
AffiliateEnabled: updatedSettings.AffiliateEnabled,
|
||||
|
||||
RiskControlEnabled: updatedSettings.RiskControlEnabled,
|
||||
@@ -2038,7 +2346,7 @@ func hasPaymentFields(req UpdateSettingsRequest) bool {
|
||||
req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil ||
|
||||
req.PaymentCancelRateLimitMax != nil || req.PaymentCancelRateLimitWindow != nil ||
|
||||
req.PaymentCancelRateLimitUnit != nil || req.PaymentCancelRateLimitMode != nil ||
|
||||
req.PaymentAlipayForceQRCode != nil
|
||||
req.PaymentAlipayForceQRCode != nil || req.PaymentAlipayMobilePrecreateDeepLink != nil
|
||||
}
|
||||
|
||||
// ensureDingTalkSyncAttributes 在保存 settings 后,按 admin 配置的 (attr key, attr name)
|
||||
|
||||
@@ -110,6 +110,7 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
model := c.Query("model")
|
||||
requestID := strings.TrimSpace(c.Query("request_id"))
|
||||
billingMode := strings.TrimSpace(c.Query("billing_mode"))
|
||||
|
||||
var requestType *int16
|
||||
@@ -142,6 +143,16 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
billingType = &bt
|
||||
}
|
||||
|
||||
var upstreamModelMismatch *bool
|
||||
if raw := strings.TrimSpace(c.Query("upstream_model_mismatch")); raw != "" {
|
||||
value, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
|
||||
return
|
||||
}
|
||||
upstreamModelMismatch = &value
|
||||
}
|
||||
|
||||
// Parse date range
|
||||
var startTime, endTime *time.Time
|
||||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||||
@@ -172,18 +183,21 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
SortOrder: c.DefaultQuery("sort_order", "desc"),
|
||||
}
|
||||
filters := usagestats.UsageLogFilters{
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Model: model,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
BillingMode: billingMode,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
ExactTotal: exactTotal,
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
RequestID: requestID,
|
||||
Model: model,
|
||||
ModelFilterSource: usagestats.ModelSourceRequested,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
BillingMode: billingMode,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
ExactTotal: exactTotal,
|
||||
}
|
||||
|
||||
records, result, err := h.usageService.ListWithFilters(c.Request.Context(), params, filters)
|
||||
@@ -273,6 +287,16 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
billingType = &bt
|
||||
}
|
||||
|
||||
var upstreamModelMismatch *bool
|
||||
if raw := strings.TrimSpace(c.Query("upstream_model_mismatch")); raw != "" {
|
||||
value, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid upstream_model_mismatch value, use true or false")
|
||||
return
|
||||
}
|
||||
upstreamModelMismatch = &value
|
||||
}
|
||||
|
||||
// Parse date range
|
||||
userTZ := c.Query("timezone")
|
||||
now := timezone.NowInUserLocation(userTZ)
|
||||
@@ -312,17 +336,19 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
|
||||
// Build filters and call GetStatsWithFilters
|
||||
filters := usagestats.UsageLogFilters{
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Model: model,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
BillingMode: billingMode,
|
||||
StartTime: &startTime,
|
||||
EndTime: &endTime,
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Model: model,
|
||||
ModelFilterSource: usagestats.ModelSourceRequested,
|
||||
RequestType: requestType,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
BillingMode: billingMode,
|
||||
UpstreamModelMismatch: upstreamModelMismatch,
|
||||
StartTime: &startTime,
|
||||
EndTime: &endTime,
|
||||
}
|
||||
|
||||
var stats *usagestats.UsageStats
|
||||
|
||||
@@ -60,6 +60,19 @@ func TestAdminUsageListRequestTypePriority(t *testing.T) {
|
||||
require.Nil(t, repo.listFilters.Stream)
|
||||
}
|
||||
|
||||
func TestAdminUsageListUsesRequestedModelForDisplayModelFilter(t *testing.T) {
|
||||
repo := &adminUsageRepoCapture{}
|
||||
router := newAdminUsageRequestTypeTestRouter(repo)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/usage?model=grok-imagine-video-1.5", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "grok-imagine-video-1.5", repo.listFilters.Model)
|
||||
require.Equal(t, usagestats.ModelSourceRequested, repo.listFilters.ModelFilterSource)
|
||||
}
|
||||
|
||||
func TestAdminUsageListInvalidRequestType(t *testing.T) {
|
||||
repo := &adminUsageRepoCapture{}
|
||||
router := newAdminUsageRequestTypeTestRouter(repo)
|
||||
@@ -94,6 +107,18 @@ func TestAdminUsageListExactTotalTrue(t *testing.T) {
|
||||
require.True(t, repo.listFilters.ExactTotal)
|
||||
}
|
||||
|
||||
func TestAdminUsageListRequestIDFilter(t *testing.T) {
|
||||
repo := &adminUsageRepoCapture{}
|
||||
router := newAdminUsageRequestTypeTestRouter(repo)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/usage?request_id=req-0123", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "req-0123", repo.listFilters.RequestID)
|
||||
}
|
||||
|
||||
func TestAdminUsageListInvalidExactTotal(t *testing.T) {
|
||||
repo := &adminUsageRepoCapture{}
|
||||
router := newAdminUsageRequestTypeTestRouter(repo)
|
||||
@@ -119,6 +144,19 @@ func TestAdminUsageStatsRequestTypePriority(t *testing.T) {
|
||||
require.Nil(t, repo.statsFilters.Stream)
|
||||
}
|
||||
|
||||
func TestAdminUsageStatsUsesRequestedModelForDisplayModelFilter(t *testing.T) {
|
||||
repo := &adminUsageRepoCapture{}
|
||||
router := newAdminUsageRequestTypeTestRouter(repo)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/usage/stats?model=grok-imagine-video-1.5", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "grok-imagine-video-1.5", repo.statsFilters.Model)
|
||||
require.Equal(t, usagestats.ModelSourceRequested, repo.statsFilters.ModelFilterSource)
|
||||
}
|
||||
|
||||
func TestAdminUsageStatsInvalidRequestType(t *testing.T) {
|
||||
repo := &adminUsageRepoCapture{}
|
||||
router := newAdminUsageRequestTypeTestRouter(repo)
|
||||
|
||||