codex-lb 0.1.5__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +1 -1
- app/core/auth/__init__.py +12 -1
- app/core/balancer/logic.py +44 -7
- app/core/clients/proxy.py +2 -4
- app/core/config/settings.py +4 -1
- app/core/plan_types.py +64 -0
- app/core/types.py +4 -2
- app/core/usage/__init__.py +5 -2
- app/core/usage/logs.py +12 -2
- app/core/usage/quota.py +64 -0
- app/core/usage/types.py +3 -2
- app/core/utils/sse.py +6 -2
- app/db/migrations/__init__.py +91 -0
- app/db/migrations/versions/__init__.py +1 -0
- app/db/migrations/versions/add_accounts_chatgpt_account_id.py +29 -0
- app/db/migrations/versions/add_accounts_reset_at.py +29 -0
- app/db/migrations/versions/add_dashboard_settings.py +31 -0
- app/db/migrations/versions/add_request_logs_reasoning_effort.py +21 -0
- app/db/migrations/versions/normalize_account_plan_types.py +17 -0
- app/db/models.py +33 -0
- app/db/session.py +85 -11
- app/dependencies.py +27 -9
- app/main.py +15 -6
- app/modules/accounts/auth_manager.py +121 -0
- app/modules/accounts/repository.py +14 -6
- app/modules/accounts/service.py +14 -9
- app/modules/health/api.py +5 -3
- app/modules/health/schemas.py +9 -0
- app/modules/oauth/service.py +9 -4
- app/modules/proxy/helpers.py +285 -0
- app/modules/proxy/load_balancer.py +86 -41
- app/modules/proxy/service.py +172 -318
- app/modules/proxy/sticky_repository.py +56 -0
- app/modules/request_logs/repository.py +6 -3
- app/modules/request_logs/schemas.py +2 -0
- app/modules/request_logs/service.py +12 -3
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +37 -0
- app/modules/settings/repository.py +40 -0
- app/modules/settings/schemas.py +13 -0
- app/modules/settings/service.py +33 -0
- app/modules/shared/schemas.py +16 -2
- app/modules/usage/schemas.py +1 -0
- app/modules/usage/service.py +23 -6
- app/modules/{proxy/usage_updater.py → usage/updater.py} +37 -8
- app/static/7.css +73 -0
- app/static/index.css +33 -4
- app/static/index.html +51 -4
- app/static/index.js +254 -32
- {codex_lb-0.1.5.dist-info → codex_lb-0.3.0.dist-info}/METADATA +2 -2
- codex_lb-0.3.0.dist-info/RECORD +97 -0
- app/modules/proxy/auth_manager.py +0 -51
- codex_lb-0.1.5.dist-info/RECORD +0 -80
- {codex_lb-0.1.5.dist-info → codex_lb-0.3.0.dist-info}/WHEEL +0 -0
- {codex_lb-0.1.5.dist-info → codex_lb-0.3.0.dist-info}/entry_points.txt +0 -0
- {codex_lb-0.1.5.dist-info → codex_lb-0.3.0.dist-info}/licenses/LICENSE +0 -0
app/static/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
oauthStart: "/api/oauth/start",
|
|
17
17
|
oauthStatus: "/api/oauth/status",
|
|
18
18
|
oauthComplete: "/api/oauth/complete",
|
|
19
|
+
settings: "/api/settings",
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
const PAGES = [
|
|
@@ -33,6 +34,13 @@
|
|
|
33
34
|
title: "Codex Load Balancer - Accounts",
|
|
34
35
|
path: "/accounts",
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
id: "settings",
|
|
39
|
+
tabId: "tab-settings",
|
|
40
|
+
label: "Settings",
|
|
41
|
+
title: "Codex Load Balancer - Settings",
|
|
42
|
+
path: "/settings",
|
|
43
|
+
},
|
|
36
44
|
];
|
|
37
45
|
|
|
38
46
|
const BASE_PATH = "/dashboard";
|
|
@@ -74,11 +82,15 @@
|
|
|
74
82
|
error: "deactivated",
|
|
75
83
|
};
|
|
76
84
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
const KNOWN_PLAN_TYPES = new Set([
|
|
86
|
+
"free",
|
|
87
|
+
"plus",
|
|
88
|
+
"pro",
|
|
89
|
+
"team",
|
|
90
|
+
"business",
|
|
91
|
+
"enterprise",
|
|
92
|
+
"edu",
|
|
93
|
+
]);
|
|
82
94
|
|
|
83
95
|
const ROUTING_LABELS = {
|
|
84
96
|
usage_weighted: "usage weighted",
|
|
@@ -92,7 +104,7 @@
|
|
|
92
104
|
timeout: "timeout",
|
|
93
105
|
upstream: "upstream",
|
|
94
106
|
rate_limit_exceeded: "rate limit",
|
|
95
|
-
usage_limit_reached: "
|
|
107
|
+
usage_limit_reached: "quota",
|
|
96
108
|
insufficient_quota: "quota",
|
|
97
109
|
usage_not_included: "quota",
|
|
98
110
|
quota_exceeded: "quota",
|
|
@@ -183,6 +195,7 @@
|
|
|
183
195
|
metrics: {
|
|
184
196
|
requests7d: 0,
|
|
185
197
|
tokensSecondaryWindow: null,
|
|
198
|
+
cachedTokensSecondaryWindow: null,
|
|
186
199
|
cost7d: 0,
|
|
187
200
|
errorRate7d: null,
|
|
188
201
|
topError: "",
|
|
@@ -238,6 +251,40 @@
|
|
|
238
251
|
return compactFormatter.format(numeric);
|
|
239
252
|
};
|
|
240
253
|
|
|
254
|
+
const formatTokensWithCached = (totalTokens, cachedInputTokens) => {
|
|
255
|
+
const total = toNumber(totalTokens);
|
|
256
|
+
if (total === null) {
|
|
257
|
+
return "--";
|
|
258
|
+
}
|
|
259
|
+
const cached = toNumber(cachedInputTokens);
|
|
260
|
+
if (cached === null || cached <= 0) {
|
|
261
|
+
return formatCompactNumber(total);
|
|
262
|
+
}
|
|
263
|
+
return `${formatCompactNumber(total)} (${formatCompactNumber(cached)} Cached)`;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const formatCachedTokensMeta = (totalTokens, cachedInputTokens) => {
|
|
267
|
+
const total = toNumber(totalTokens);
|
|
268
|
+
const cached = toNumber(cachedInputTokens);
|
|
269
|
+
if (total === null || total <= 0 || cached === null || cached <= 0) {
|
|
270
|
+
return "Cached: --";
|
|
271
|
+
}
|
|
272
|
+
const percent = Math.min(100, Math.max(0, (cached / total) * 100));
|
|
273
|
+
return `Cached: ${formatCompactNumber(cached)} (${Math.round(percent)}%)`;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const formatModelLabel = (model, reasoningEffort) => {
|
|
277
|
+
const base = (model || "").trim();
|
|
278
|
+
if (!base) {
|
|
279
|
+
return "--";
|
|
280
|
+
}
|
|
281
|
+
const effort = (reasoningEffort || "").trim();
|
|
282
|
+
if (!effort) {
|
|
283
|
+
return base;
|
|
284
|
+
}
|
|
285
|
+
return `${base} (${effort})`;
|
|
286
|
+
};
|
|
287
|
+
|
|
241
288
|
const formatCurrency = (value) => {
|
|
242
289
|
const numeric = toNumber(value);
|
|
243
290
|
if (numeric === null) {
|
|
@@ -444,7 +491,19 @@
|
|
|
444
491
|
REQUEST_STATUS_LABELS[status] || "Unknown";
|
|
445
492
|
const requestStatusClass = (status) =>
|
|
446
493
|
REQUEST_STATUS_CLASSES[status] || "deactivated";
|
|
447
|
-
const
|
|
494
|
+
const normalizePlanType = (plan) => {
|
|
495
|
+
if (plan === null || plan === undefined) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const value = String(plan).trim().toLowerCase();
|
|
499
|
+
return KNOWN_PLAN_TYPES.has(value) ? value : null;
|
|
500
|
+
};
|
|
501
|
+
const titleCase = (value) =>
|
|
502
|
+
value ? value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() : "";
|
|
503
|
+
const planLabel = (plan) => {
|
|
504
|
+
const normalized = normalizePlanType(plan);
|
|
505
|
+
return normalized ? titleCase(normalized) : "Unknown";
|
|
506
|
+
};
|
|
448
507
|
const routingLabel = (strategy) => ROUTING_LABELS[strategy] || "unknown";
|
|
449
508
|
const errorLabel = (code) => ERROR_LABELS[code] || "--";
|
|
450
509
|
const progressClass = (status) => PROGRESS_CLASS_BY_STATUS[status] || "";
|
|
@@ -549,8 +608,10 @@
|
|
|
549
608
|
accountId: entry.accountId,
|
|
550
609
|
requestId: entry.requestId,
|
|
551
610
|
model: entry.model,
|
|
611
|
+
reasoningEffort: entry.reasoningEffort ?? null,
|
|
552
612
|
status: entry.status,
|
|
553
613
|
tokens: toNumber(entry.tokens),
|
|
614
|
+
cachedInputTokens: toNumber(entry.cachedInputTokens),
|
|
554
615
|
cost: toNumber(entry.costUsd),
|
|
555
616
|
errorCode: entry.errorCode ?? null,
|
|
556
617
|
errorMessage: entry.errorMessage ?? null,
|
|
@@ -579,17 +640,21 @@
|
|
|
579
640
|
const secondaryRemainingPercent = toNumber(
|
|
580
641
|
secondaryRow?.remainingPercentAvg,
|
|
581
642
|
);
|
|
643
|
+
const mergedSecondaryRemaining =
|
|
644
|
+
secondaryRemainingPercent ??
|
|
645
|
+
account.usage?.secondaryRemainingPercent ??
|
|
646
|
+
0;
|
|
647
|
+
const mergedPrimaryRemaining =
|
|
648
|
+
primaryRemainingPercent ??
|
|
649
|
+
account.usage?.primaryRemainingPercent ??
|
|
650
|
+
0;
|
|
651
|
+
const effectivePrimaryRemaining =
|
|
652
|
+
mergedSecondaryRemaining <= 0 ? 0 : mergedPrimaryRemaining;
|
|
582
653
|
return {
|
|
583
654
|
...account,
|
|
584
655
|
usage: {
|
|
585
|
-
primaryRemainingPercent:
|
|
586
|
-
|
|
587
|
-
account.usage?.primaryRemainingPercent ??
|
|
588
|
-
0,
|
|
589
|
-
secondaryRemainingPercent:
|
|
590
|
-
secondaryRemainingPercent ??
|
|
591
|
-
account.usage?.secondaryRemainingPercent ??
|
|
592
|
-
0,
|
|
656
|
+
primaryRemainingPercent: effectivePrimaryRemaining,
|
|
657
|
+
secondaryRemainingPercent: mergedSecondaryRemaining,
|
|
593
658
|
},
|
|
594
659
|
resetAtPrimary: account.resetAtPrimary ?? null,
|
|
595
660
|
resetAtSecondary: account.resetAtSecondary ?? null,
|
|
@@ -625,6 +690,7 @@
|
|
|
625
690
|
accountId: entry.accountId,
|
|
626
691
|
capacityCredits: toNumber(entry.capacityCredits) || 0,
|
|
627
692
|
remainingCredits: toNumber(entry.remainingCredits) || 0,
|
|
693
|
+
remainingPercentAvg: toNumber(entry.remainingPercentAvg),
|
|
628
694
|
})),
|
|
629
695
|
};
|
|
630
696
|
};
|
|
@@ -638,6 +704,7 @@
|
|
|
638
704
|
const metrics = summary?.metrics || {};
|
|
639
705
|
const requests7d = toNumber(metrics.requests7d) ?? 0;
|
|
640
706
|
const tokensSecondaryWindow = toNumber(metrics.tokensSecondaryWindow);
|
|
707
|
+
const cachedTokensSecondaryWindow = toNumber(metrics.cachedTokensSecondaryWindow);
|
|
641
708
|
const errorRate7d = toNumber(metrics.errorRate7d);
|
|
642
709
|
const topError = metrics.topError || "";
|
|
643
710
|
return {
|
|
@@ -649,6 +716,7 @@
|
|
|
649
716
|
metrics: {
|
|
650
717
|
requests7d,
|
|
651
718
|
tokensSecondaryWindow,
|
|
719
|
+
cachedTokensSecondaryWindow,
|
|
652
720
|
cost7d: toNumber(summary?.cost?.totalUsd7d) || 0,
|
|
653
721
|
errorRate7d,
|
|
654
722
|
topError,
|
|
@@ -683,7 +751,7 @@
|
|
|
683
751
|
return acc;
|
|
684
752
|
}, {});
|
|
685
753
|
|
|
686
|
-
const buildRemainingItems = (entries, accounts, capacity) => {
|
|
754
|
+
const buildRemainingItems = (entries, accounts, capacity, windowKey) => {
|
|
687
755
|
const accountMap = new Map(
|
|
688
756
|
(accounts || []).map((account) => [account.id, account]),
|
|
689
757
|
);
|
|
@@ -691,7 +759,23 @@
|
|
|
691
759
|
const account = accountMap.get(entry.accountId);
|
|
692
760
|
const label = account ? account.email : entry.accountId;
|
|
693
761
|
const value = toNumber(entry.remainingCredits) || 0;
|
|
694
|
-
const
|
|
762
|
+
const percentFromApi = toNumber(entry.remainingPercentAvg);
|
|
763
|
+
const percentFromAccount =
|
|
764
|
+
windowKey === "primary"
|
|
765
|
+
? toNumber(account?.usage?.primaryRemainingPercent)
|
|
766
|
+
: windowKey === "secondary"
|
|
767
|
+
? toNumber(account?.usage?.secondaryRemainingPercent)
|
|
768
|
+
: null;
|
|
769
|
+
const entryCapacity = toNumber(entry.capacityCredits) || 0;
|
|
770
|
+
const denominator = entryCapacity > 0 ? entryCapacity : capacity;
|
|
771
|
+
const rawPercent =
|
|
772
|
+
percentFromApi !== null
|
|
773
|
+
? percentFromApi
|
|
774
|
+
: percentFromAccount !== null
|
|
775
|
+
? percentFromAccount
|
|
776
|
+
: denominator > 0
|
|
777
|
+
? (value / denominator) * 100
|
|
778
|
+
: 0;
|
|
695
779
|
const remainingPercent = Math.min(100, Math.max(0, rawPercent));
|
|
696
780
|
return {
|
|
697
781
|
accountId: entry.accountId,
|
|
@@ -718,6 +802,38 @@
|
|
|
718
802
|
}));
|
|
719
803
|
};
|
|
720
804
|
|
|
805
|
+
const buildSecondaryExhaustedIndex = (accounts) => {
|
|
806
|
+
const exhausted = new Set();
|
|
807
|
+
(accounts || []).forEach((account) => {
|
|
808
|
+
const remaining = toNumber(account?.usage?.secondaryRemainingPercent);
|
|
809
|
+
if (remaining !== null && remaining <= 0 && account?.id) {
|
|
810
|
+
exhausted.add(account.id);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
return exhausted;
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const applySecondaryExhaustedToPrimary = (entries, exhaustedIds) => {
|
|
817
|
+
if (!entries?.length || !exhaustedIds?.size) {
|
|
818
|
+
return entries || [];
|
|
819
|
+
}
|
|
820
|
+
return entries.map((entry) => {
|
|
821
|
+
if (entry?.accountId && exhaustedIds.has(entry.accountId)) {
|
|
822
|
+
return {
|
|
823
|
+
...entry,
|
|
824
|
+
remainingCredits: 0,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return entry;
|
|
828
|
+
});
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const sumRemainingCredits = (entries) =>
|
|
832
|
+
(entries || []).reduce(
|
|
833
|
+
(acc, entry) => acc + (toNumber(entry?.remainingCredits) || 0),
|
|
834
|
+
0,
|
|
835
|
+
);
|
|
836
|
+
|
|
721
837
|
const buildDonutGradient = (items, total) => {
|
|
722
838
|
if (!items.length || total <= 0) {
|
|
723
839
|
return `conic-gradient(${CONSUMED_COLOR} 0 100%)`;
|
|
@@ -760,6 +876,7 @@
|
|
|
760
876
|
const statusCounts = countByStatus(accounts);
|
|
761
877
|
const secondaryWindowMinutes =
|
|
762
878
|
state.dashboardData.usage?.secondary?.windowMinutes ?? null;
|
|
879
|
+
const secondaryExhaustedAccounts = buildSecondaryExhaustedIndex(accounts);
|
|
763
880
|
|
|
764
881
|
const badges = ["active", "paused", "limited", "exceeded", "deactivated"]
|
|
765
882
|
.map((status) => {
|
|
@@ -779,7 +896,10 @@
|
|
|
779
896
|
{
|
|
780
897
|
title: `Tokens (${formatWindowLabel("secondary", secondaryWindowMinutes)})`,
|
|
781
898
|
value: formatCompactNumber(metrics.tokensSecondaryWindow),
|
|
782
|
-
meta:
|
|
899
|
+
meta: formatCachedTokensMeta(
|
|
900
|
+
metrics.tokensSecondaryWindow,
|
|
901
|
+
metrics.cachedTokensSecondaryWindow,
|
|
902
|
+
),
|
|
783
903
|
},
|
|
784
904
|
{
|
|
785
905
|
title: "Cost (7d)",
|
|
@@ -807,18 +927,37 @@
|
|
|
807
927
|
resetAt: null,
|
|
808
928
|
byAccount: [],
|
|
809
929
|
};
|
|
810
|
-
const
|
|
930
|
+
const rawEntries = usage.byAccount || [];
|
|
931
|
+
const hasPrimaryAdjustments =
|
|
932
|
+
window.key === "primary" &&
|
|
933
|
+
rawEntries.some(
|
|
934
|
+
(entry) =>
|
|
935
|
+
entry?.accountId && secondaryExhaustedAccounts.has(entry.accountId),
|
|
936
|
+
);
|
|
937
|
+
const entries =
|
|
938
|
+
window.key === "primary"
|
|
939
|
+
? applySecondaryExhaustedToPrimary(
|
|
940
|
+
rawEntries,
|
|
941
|
+
secondaryExhaustedAccounts,
|
|
942
|
+
)
|
|
943
|
+
: rawEntries;
|
|
944
|
+
const remaining =
|
|
945
|
+
hasPrimaryAdjustments
|
|
946
|
+
? sumRemainingCredits(entries)
|
|
947
|
+
: toNumber(usage.remaining) || 0;
|
|
811
948
|
const capacity = Math.max(remaining, toNumber(usage.capacity) || 0);
|
|
812
949
|
const consumed = Math.max(0, capacity - remaining);
|
|
813
950
|
const items = buildRemainingItems(
|
|
814
|
-
|
|
951
|
+
entries,
|
|
815
952
|
accounts,
|
|
816
953
|
capacity,
|
|
954
|
+
window.key,
|
|
817
955
|
);
|
|
818
956
|
const gradient = buildDonutGradient(items, capacity);
|
|
819
957
|
const legendItems = items.map((item) => ({
|
|
820
958
|
label: item.label,
|
|
821
|
-
|
|
959
|
+
detailLabel: "Remaining",
|
|
960
|
+
detailValue: formatPercent(item.remainingPercent),
|
|
822
961
|
color: item.color,
|
|
823
962
|
}));
|
|
824
963
|
if (capacity > 0 && consumed > 0) {
|
|
@@ -828,7 +967,8 @@
|
|
|
828
967
|
);
|
|
829
968
|
legendItems.push({
|
|
830
969
|
label: "Consumed",
|
|
831
|
-
|
|
970
|
+
detailLabel: "",
|
|
971
|
+
detailValue: formatPercent(consumedPercent),
|
|
832
972
|
color: CONSUMED_COLOR,
|
|
833
973
|
});
|
|
834
974
|
}
|
|
@@ -868,17 +1008,18 @@
|
|
|
868
1008
|
const requests = state.dashboardData.recentRequests.map((request) => {
|
|
869
1009
|
const rawError = request.errorMessage || request.errorCode || "";
|
|
870
1010
|
const accountLabel = formatAccountLabel(request.accountId, accounts);
|
|
1011
|
+
const modelLabel = formatModelLabel(request.model, request.reasoningEffort);
|
|
871
1012
|
return {
|
|
872
1013
|
key: `${request.requestId}-${request.timestamp}`,
|
|
873
1014
|
requestId: request.requestId,
|
|
874
1015
|
time: formatTimeLong(request.timestamp),
|
|
875
1016
|
account: accountLabel,
|
|
876
|
-
model:
|
|
1017
|
+
model: modelLabel,
|
|
877
1018
|
status: {
|
|
878
1019
|
class: requestStatusClass(request.status),
|
|
879
1020
|
label: requestStatusLabel(request.status),
|
|
880
1021
|
},
|
|
881
|
-
tokens:
|
|
1022
|
+
tokens: formatTokensWithCached(request.tokens, request.cachedInputTokens),
|
|
882
1023
|
cost: formatCurrency(request.cost),
|
|
883
1024
|
error: rawError ? truncateText(rawError, 80) : "--",
|
|
884
1025
|
errorTitle: rawError,
|
|
@@ -1015,6 +1156,20 @@
|
|
|
1015
1156
|
return responsePayload;
|
|
1016
1157
|
};
|
|
1017
1158
|
|
|
1159
|
+
const putJson = async (url, payload, label) => {
|
|
1160
|
+
const response = await fetch(url, {
|
|
1161
|
+
method: "PUT",
|
|
1162
|
+
headers: { "Content-Type": "application/json" },
|
|
1163
|
+
body: JSON.stringify(payload || {}),
|
|
1164
|
+
});
|
|
1165
|
+
const responsePayload = await readResponsePayload(response);
|
|
1166
|
+
if (!response.ok) {
|
|
1167
|
+
const message = extractErrorMessage(responsePayload);
|
|
1168
|
+
throw new Error(message || `Failed to ${label} (${response.status})`);
|
|
1169
|
+
}
|
|
1170
|
+
return responsePayload;
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1018
1173
|
const deleteJson = async (url, label) => {
|
|
1019
1174
|
const response = await fetch(url, { method: "DELETE" });
|
|
1020
1175
|
const responsePayload = await readResponsePayload(response);
|
|
@@ -1039,6 +1194,16 @@
|
|
|
1039
1194
|
const fetchRequestLogs = async (limit) =>
|
|
1040
1195
|
fetchJson(API_ENDPOINTS.requestLogs(limit), "request logs");
|
|
1041
1196
|
|
|
1197
|
+
const normalizeSettingsPayload = (payload) => ({
|
|
1198
|
+
stickyThreadsEnabled: Boolean(payload?.stickyThreadsEnabled),
|
|
1199
|
+
preferEarlierResetAccounts: Boolean(payload?.preferEarlierResetAccounts),
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
const fetchSettings = async () => {
|
|
1203
|
+
const payload = await fetchJson(API_ENDPOINTS.settings, "settings");
|
|
1204
|
+
return normalizeSettingsPayload(payload);
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1042
1207
|
const registerApp = () => {
|
|
1043
1208
|
Alpine.data("feApp", () => ({
|
|
1044
1209
|
view: "dashboard",
|
|
@@ -1047,6 +1212,11 @@
|
|
|
1047
1212
|
ui: createUiConfig(),
|
|
1048
1213
|
dashboardData: createEmptyDashboardData(),
|
|
1049
1214
|
dashboard: createEmptyDashboardView(),
|
|
1215
|
+
settings: {
|
|
1216
|
+
stickyThreadsEnabled: false,
|
|
1217
|
+
preferEarlierResetAccounts: false,
|
|
1218
|
+
isSaving: false,
|
|
1219
|
+
},
|
|
1050
1220
|
accounts: {
|
|
1051
1221
|
selectedId: "",
|
|
1052
1222
|
rows: [],
|
|
@@ -1142,12 +1312,14 @@
|
|
|
1142
1312
|
primaryResult,
|
|
1143
1313
|
secondaryResult,
|
|
1144
1314
|
requestLogsResult,
|
|
1315
|
+
settingsResult,
|
|
1145
1316
|
] = await Promise.allSettled([
|
|
1146
1317
|
fetchAccounts(),
|
|
1147
1318
|
fetchUsageSummary(),
|
|
1148
1319
|
fetchUsageWindow("primary"),
|
|
1149
1320
|
fetchUsageWindow("secondary"),
|
|
1150
1321
|
fetchRequestLogs(50),
|
|
1322
|
+
fetchSettings(),
|
|
1151
1323
|
]);
|
|
1152
1324
|
const errors = [];
|
|
1153
1325
|
if (accountsResult.status !== "fulfilled") {
|
|
@@ -1180,6 +1352,12 @@
|
|
|
1180
1352
|
errors.push(requestLogsResult.reason);
|
|
1181
1353
|
}
|
|
1182
1354
|
|
|
1355
|
+
const settings =
|
|
1356
|
+
settingsResult.status === "fulfilled" ? settingsResult.value : null;
|
|
1357
|
+
if (settingsResult.status === "rejected") {
|
|
1358
|
+
errors.push(settingsResult.reason);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1183
1361
|
const mergedAccounts = mergeUsageIntoAccounts(
|
|
1184
1362
|
accountsResult.value,
|
|
1185
1363
|
primaryUsage,
|
|
@@ -1192,6 +1370,7 @@
|
|
|
1192
1370
|
primaryUsage,
|
|
1193
1371
|
secondaryUsage,
|
|
1194
1372
|
requestLogs,
|
|
1373
|
+
settings,
|
|
1195
1374
|
},
|
|
1196
1375
|
preferredId,
|
|
1197
1376
|
);
|
|
@@ -1236,11 +1415,54 @@
|
|
|
1236
1415
|
primaryUsage: data.primaryUsage,
|
|
1237
1416
|
secondaryUsage: data.secondaryUsage,
|
|
1238
1417
|
requestLogs: data.requestLogs,
|
|
1418
|
+
settings: data.settings,
|
|
1239
1419
|
});
|
|
1420
|
+
if (data.settings) {
|
|
1421
|
+
this.settings.stickyThreadsEnabled = Boolean(
|
|
1422
|
+
data.settings.stickyThreadsEnabled,
|
|
1423
|
+
);
|
|
1424
|
+
this.settings.preferEarlierResetAccounts = Boolean(
|
|
1425
|
+
data.settings.preferEarlierResetAccounts,
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1240
1428
|
this.ui.usageWindows = buildUsageWindowConfig(data.summary);
|
|
1241
1429
|
this.dashboard = buildDashboardView(this);
|
|
1242
1430
|
this.syncAccountSearchSelection();
|
|
1243
1431
|
},
|
|
1432
|
+
async saveSettings() {
|
|
1433
|
+
if (this.settings.isSaving) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
this.settings.isSaving = true;
|
|
1437
|
+
try {
|
|
1438
|
+
const payload = {
|
|
1439
|
+
stickyThreadsEnabled: this.settings.stickyThreadsEnabled,
|
|
1440
|
+
preferEarlierResetAccounts: this.settings.preferEarlierResetAccounts,
|
|
1441
|
+
};
|
|
1442
|
+
const updated = await putJson(
|
|
1443
|
+
API_ENDPOINTS.settings,
|
|
1444
|
+
payload,
|
|
1445
|
+
"save settings",
|
|
1446
|
+
);
|
|
1447
|
+
const normalized = normalizeSettingsPayload(updated);
|
|
1448
|
+
this.settings.stickyThreadsEnabled = normalized.stickyThreadsEnabled;
|
|
1449
|
+
this.settings.preferEarlierResetAccounts =
|
|
1450
|
+
normalized.preferEarlierResetAccounts;
|
|
1451
|
+
this.openMessageBox({
|
|
1452
|
+
tone: "success",
|
|
1453
|
+
title: "Settings saved",
|
|
1454
|
+
message: "Routing settings updated.",
|
|
1455
|
+
});
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
this.openMessageBox({
|
|
1458
|
+
tone: "error",
|
|
1459
|
+
title: "Settings save failed",
|
|
1460
|
+
message: error.message || "Failed to save settings.",
|
|
1461
|
+
});
|
|
1462
|
+
} finally {
|
|
1463
|
+
this.settings.isSaving = false;
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1244
1466
|
focusAccountSearch() {
|
|
1245
1467
|
this.$refs.accountSearch?.focus();
|
|
1246
1468
|
},
|
|
@@ -1677,15 +1899,15 @@
|
|
|
1677
1899
|
const items =
|
|
1678
1900
|
this.view === "accounts"
|
|
1679
1901
|
? [
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1902
|
+
`Selection: ${this.accounts.selectedId || "--"}`,
|
|
1903
|
+
`Rotation: ${this.dashboardData.routing?.rotationEnabled ? "enabled" : "disabled"}`,
|
|
1904
|
+
`Last sync: ${lastSync}`,
|
|
1905
|
+
]
|
|
1684
1906
|
: [
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1907
|
+
`Last sync: ${lastSync}`,
|
|
1908
|
+
`Routing: ${routingLabel(this.dashboardData.routing?.strategy)}`,
|
|
1909
|
+
`Backend: ${this.backendPath}`,
|
|
1910
|
+
];
|
|
1689
1911
|
if (this.importState.isLoading) {
|
|
1690
1912
|
items.unshift(
|
|
1691
1913
|
`Importing ${this.importState.fileName || "auth.json"}...`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-lb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Codex load balancer and proxy for ChatGPT accounts with usage dashboard
|
|
5
5
|
Author-email: Soju06 <qlskssk@gmail.com>
|
|
6
6
|
Maintainer-email: Soju06 <qlskssk@gmail.com>
|
|
@@ -39,7 +39,7 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
39
39
|
Classifier: Topic :: System :: Networking
|
|
40
40
|
Requires-Python: >=3.13
|
|
41
41
|
Requires-Dist: aiohttp-retry>=2.9.1
|
|
42
|
-
Requires-Dist: aiohttp>=3.13.
|
|
42
|
+
Requires-Dist: aiohttp>=3.13.3
|
|
43
43
|
Requires-Dist: aiosqlite>=0.22.1
|
|
44
44
|
Requires-Dist: cryptography>=46.0.3
|
|
45
45
|
Requires-Dist: fastapi[standard]>=0.128.0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
app/__init__.py,sha256=uqZSnn_VEL8TIUxsYqdf4zA2ByJYfjA06fArVAzrHFo,89
|
|
2
|
+
app/cli.py,sha256=gkIAkYOT9SbQjUDnVmwhVKZeKjL3YJCMrOjFINwBx54,544
|
|
3
|
+
app/dependencies.py,sha256=kfB_TxeZve_cnBxhOHZezKwkOwTNFwoHzNCJDUkGJD8,4377
|
|
4
|
+
app/main.py,sha256=wCELolhaBfJCEfFfSELNTQ5hZ1Xo-G5Wa3vMkIOY6gk,4665
|
|
5
|
+
app/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
app/core/crypto.py,sha256=zUz2GVqXigzXB5zX2Diq2cR41Kl37bBqxJiZmWGiIcY,1145
|
|
7
|
+
app/core/errors.py,sha256=go4Q5vv6_Rt8ZS230Mp436yCViEKu_xICylGF0gvGJg,1805
|
|
8
|
+
app/core/plan_types.py,sha256=6if0Zj2sTrtvpijkiJ0AtfkY0EfwARjKQetcD7Q8DM8,1548
|
|
9
|
+
app/core/types.py,sha256=cVWOt-cYNjleEBvPYQVOO-tru38mZYsrBrlYRUfXPxg,208
|
|
10
|
+
app/core/auth/__init__.py,sha256=5BmHcd8P05YmMpnKAxokWVkOkZK2Opaf04qgEk9P8k4,3257
|
|
11
|
+
app/core/auth/models.py,sha256=iyygu0uHK7fu8txyIyCkXmlgqYPbqEZu3IFysD1t-gk,1557
|
|
12
|
+
app/core/auth/refresh.py,sha256=jhYxT2mQFX4CCvuBQfYNcI53iOtWa0vSkTuZAfbpd9k,5105
|
|
13
|
+
app/core/balancer/__init__.py,sha256=bGy7gITRPObt6P9NdG3pg8t3qWoSW0XWoADqfNYrhdg,405
|
|
14
|
+
app/core/balancer/logic.py,sha256=N8skZL7ye_qLtUMBcHjgqSjsuCKWFOPdBposu_DLxuc,6730
|
|
15
|
+
app/core/balancer/types.py,sha256=gDgjlTy-NH3YhHYl2-YYpIabnckN9Q8-4cRy6S1u0K4,191
|
|
16
|
+
app/core/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
app/core/clients/http.py,sha256=yfFwsaIZNbSmNtyV02WnMKn00JdoNNSlSIx5xB3QY4o,987
|
|
18
|
+
app/core/clients/oauth.py,sha256=XgAzQVAMudBUCQ9nGKUC9N7zagSiawBdhIVmxf9HHwQ,11832
|
|
19
|
+
app/core/clients/proxy.py,sha256=ZWs0TBYcOgDz_jZ3tucDsRiqQ_9TGxU1oPGiI1mu-bE,9378
|
|
20
|
+
app/core/clients/usage.py,sha256=kG7TXqmy8IX9m4wJx5fOGDB2hqunivOU6o2xfoXCGy4,4800
|
|
21
|
+
app/core/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
app/core/config/settings.py,sha256=h6JoHfsaqZpUmv8RPKw38zv45eHyjv53PbHCw3pK7h8,2648
|
|
23
|
+
app/core/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
app/core/openai/models.py,sha256=SzVPqmp9IDbJTW6gLGHeLxrpAmFehJPSC4fVsqzEmQA,3344
|
|
25
|
+
app/core/openai/parsing.py,sha256=VuE1OyPAv1umrSbkzqa6dcjPYfk00isOVB3O0xUPLBw,1537
|
|
26
|
+
app/core/openai/requests.py,sha256=Nnd4ZtxUtXP-jJ7LGKpPf_JDBIFo-X9k0qMw9SyANTc,1675
|
|
27
|
+
app/core/usage/__init__.py,sha256=8SBpiClJR2wl653Cj1DTvmVUvi2jzU6kKXJtq91ofmE,5726
|
|
28
|
+
app/core/usage/logs.py,sha256=8TrE9nccEjIJslMfAk8AJLmX_Zzeksu8qMTvrGwePx0,2129
|
|
29
|
+
app/core/usage/models.py,sha256=FtBQx4Rb7jpwcqxmGXxg7RTVV17LK1QOSWaJIkaaNoQ,878
|
|
30
|
+
app/core/usage/pricing.py,sha256=6p8rJ26Gk61mz2t_h9sa0T7NiPDUTiNpzoDewMzT6E0,5464
|
|
31
|
+
app/core/usage/quota.py,sha256=rM6p5NshxrAO5sXcSIE69cFY8ChQXfa-_9WnjAI1uMU,2235
|
|
32
|
+
app/core/usage/types.py,sha256=EbZQjJuh2y2cMztRYf-Qe15AkPQd08LLu7WSlC4PWTY,2172
|
|
33
|
+
app/core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
app/core/utils/request_id.py,sha256=gxafI-Se8dRQTir-HNGelMzC9S2gwYZntTkVzEqzp_I,705
|
|
35
|
+
app/core/utils/retry.py,sha256=UmBap1Wh-CBT7r4fHzVb_PI9-LR9-HjUtDzRnhRjP2U,822
|
|
36
|
+
app/core/utils/sse.py,sha256=DJMOU4vW5Ir_4WeL5t5t7i33aRMnqVPU0eQvGn4sBv8,537
|
|
37
|
+
app/core/utils/time.py,sha256=B6FfSe43Eq_puE6eourly1X3gajyihK2VOAwJ8M3wyI,497
|
|
38
|
+
app/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
+
app/db/models.py,sha256=Wp7OUMYtqkT-8BD-aoYUsdZgTCMMXg-e0VU1vSKMRg8,5244
|
|
40
|
+
app/db/session.py,sha256=T_UdGcqzn5YXo-fuRW8s5xJQvOuJsV4eTSZE0_V2C44,3910
|
|
41
|
+
app/db/migrations/__init__.py,sha256=hRZVLgAQfP-okuhIiKkQtwfnPtBvYXcLJTqg0Yex1Vc,2633
|
|
42
|
+
app/db/migrations/versions/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
43
|
+
app/db/migrations/versions/add_accounts_chatgpt_account_id.py,sha256=5QHfko2f7dq0G34Cqe8CkcKbQor2W5LegfnVu_HgZN8,978
|
|
44
|
+
app/db/migrations/versions/add_accounts_reset_at.py,sha256=cO4Z4LU82JZADkBkYygxzkKdZrsNhGp899zPEtUBvtk,958
|
|
45
|
+
app/db/migrations/versions/add_dashboard_settings.py,sha256=SA6GpVU2-jStSyzKK1osLCTO_WKr4WF58fjNdP900Ec,778
|
|
46
|
+
app/db/migrations/versions/add_request_logs_reasoning_effort.py,sha256=jBy4rH5R2yCAzo5N7KbiA3tbjsoxxDjAy95Poe_qen4,771
|
|
47
|
+
app/db/migrations/versions/normalize_account_plan_types.py,sha256=WpPhkJ2gtpgw5mbErj2He9ahjeqNV3Z7c9RmmMjMmtM,575
|
|
48
|
+
app/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
+
app/modules/accounts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
|
+
app/modules/accounts/api.py,sha256=rSkYrg3_p_JE1kHZ4xXa5lBFTdT40v1oNAmlTDGOguY,2807
|
|
51
|
+
app/modules/accounts/auth_manager.py,sha256=YUKdeHbszRLYZ4uRQzvk6ICk2cB73LjlJMEiWphBWWw,4682
|
|
52
|
+
app/modules/accounts/repository.py,sha256=xOj47_BeYzVPj_WBejrlGtlTDd0tNR4o6sZIrM8QriE,3382
|
|
53
|
+
app/modules/accounts/schemas.py,sha256=gtlbPg5uxM3t_V5JxCL6eP-UaU6TSE0UoX2yIpxM_a0,1659
|
|
54
|
+
app/modules/accounts/service.py,sha256=M3_SRD21yp0YN820IHdqNIlZsynNsSUN6TO6RcESDeI,8973
|
|
55
|
+
app/modules/health/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
|
+
app/modules/health/api.py,sha256=CSs09qeEy0DjM9twmmQGg-kNt2J4XNFBX72CwLuK-Gs,297
|
|
57
|
+
app/modules/health/schemas.py,sha256=nnF32SQbR-5rLSaRtwwiC3ZOhjSnRHPgr2cagwW2rqo,177
|
|
58
|
+
app/modules/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
+
app/modules/oauth/api.py,sha256=Wu_DEXgN3hP9Si6MMkl6-0v_Blwjahqwtun1iP7DjVk,1877
|
|
60
|
+
app/modules/oauth/schemas.py,sha256=sdDKP7u9bO87lcZXjK7uSokurHPS22nN2p_jkw9iEBc,777
|
|
61
|
+
app/modules/oauth/service.py,sha256=fEzUtoq1g-20NgWZlB_maA2XB-scMa0HJV24SxZPnqQ,12908
|
|
62
|
+
app/modules/oauth/templates/oauth_success.html,sha256=YNSGUIozcZEJQjpFtM2sgF4n8jqfbmx8LRwdXTraym4,3799
|
|
63
|
+
app/modules/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
+
app/modules/proxy/api.py,sha256=BR_qNlNZg2Ft5GYQ-7AzlLlJFf6RVJmlzJzFr_KHUIc,2921
|
|
65
|
+
app/modules/proxy/helpers.py,sha256=-XVTNTpkDvJ3KfsuGHztfUVBGBFm2H-AaNTNH2dNe6o,8739
|
|
66
|
+
app/modules/proxy/load_balancer.py,sha256=Forib096qbWud11aKw9TRbBN1bIQjOmE3ZYY62vhVYk,9831
|
|
67
|
+
app/modules/proxy/schemas.py,sha256=55pXtUCl2R_93kAPOJJ7Ji4Jn3qVu10vq2KSCCkNdp4,2748
|
|
68
|
+
app/modules/proxy/service.py,sha256=wXXXu4Nd88RDzY1EQvA7OZPH5EW1F9hCINB1vGjT1q8,23647
|
|
69
|
+
app/modules/proxy/sticky_repository.py,sha256=peFaAnaCVr064W-Sh0Kjvz-MuPbBfrh86RLQOGJ6qqs,2177
|
|
70
|
+
app/modules/proxy/types.py,sha256=iqEyoO8vGr8N5oEzUSvVWCai7UZbJAU62IvO7JNS9qs,927
|
|
71
|
+
app/modules/request_logs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
|
+
app/modules/request_logs/api.py,sha256=6nV2Uv_hnK7WI3gNpKrgTx4MyUQIXk1QxKp40nij0Xo,1037
|
|
73
|
+
app/modules/request_logs/repository.py,sha256=iIaXGIG1qjol4UiTsVASIJN0g5zIdMg6tRNolkXZ0pI,3460
|
|
74
|
+
app/modules/request_logs/schemas.py,sha256=Xlpk2hEVCQxA2TI7dh4EA6G0kSfzYcT_LlvyTVLIW2w,675
|
|
75
|
+
app/modules/request_logs/service.py,sha256=8IvC4GIhPh6FVQGkuww1Ej1onFanObnX7mY3tDEy99A,2643
|
|
76
|
+
app/modules/settings/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
77
|
+
app/modules/settings/api.py,sha256=dbnvKUF4lgX5ipxOik4SJBeJ6hctJKe3lcT2Uf0F5BQ,1461
|
|
78
|
+
app/modules/settings/repository.py,sha256=r-GjuURPcftCx5HnFOZKFpv34g9bvy0m53owVCc3tpI,1204
|
|
79
|
+
app/modules/settings/schemas.py,sha256=V27alhIa4LtJHNPuZIj_EGX0v0kzn0v3esn27s1z_bc,343
|
|
80
|
+
app/modules/settings/service.py,sha256=FUayQsKEsZf2roe84j4HRH7vTXXi4W071IZlOCOkjpM,1188
|
|
81
|
+
app/modules/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
82
|
+
app/modules/shared/schemas.py,sha256=2JgPY4N6vRXX7C0ETackykmjKgTjobZYBMWiwpPfLic,650
|
|
83
|
+
app/modules/usage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
84
|
+
app/modules/usage/api.py,sha256=jpPc2VSjmiP6DjRZvotHCh_trLINOngSyTdS7MY9fgg,1108
|
|
85
|
+
app/modules/usage/repository.py,sha256=DtJI4kgajW7YUJ0JKJjdNCPBXT_fdBwqDoepi9aznyA,4791
|
|
86
|
+
app/modules/usage/schemas.py,sha256=eCgunQOvQeYEqv9IecjunONPVLpg2MPn_YGzsnBTcpQ,1633
|
|
87
|
+
app/modules/usage/service.py,sha256=8-XX8m4TgQteum-a53l0DS-EL2NnC4r9artehfFltvM,10315
|
|
88
|
+
app/modules/usage/updater.py,sha256=VAeRFvgfpZLCTGxfikefw35ecs8Ja7EoWldIldXDjmE,6783
|
|
89
|
+
app/static/7.css,sha256=NjFS937GS7Wg-LTSon5eoiZaHo9fAIbHZ3ikkEDg4Qw,82378
|
|
90
|
+
app/static/index.css,sha256=amajcY4psagBwwg5FqgINHNebEHdL1IPRmBGG4fcm24,9271
|
|
91
|
+
app/static/index.html,sha256=JPEj3IsVKsK2ub6-_hxKj_Yr9OnRuobHsX8plH9SzDE,26860
|
|
92
|
+
app/static/index.js,sha256=p_BZxgROGRLXxy9JGAB03SUARDwHesbmUy0LAVq-Z7w,59013
|
|
93
|
+
codex_lb-0.3.0.dist-info/METADATA,sha256=RuWBdjeptPK-OigdjI-mbDVVcj2QvTRUU7s1RbwajKU,3828
|
|
94
|
+
codex_lb-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
95
|
+
codex_lb-0.3.0.dist-info/entry_points.txt,sha256=SEa5T6Uz2Fhy574No6Y0XyGmYi3PXLrhu2xStJTqyI8,42
|
|
96
|
+
codex_lb-0.3.0.dist-info/licenses/LICENSE,sha256=cHPibxiL0TXwrUX_kNY6ym544EX1UCzKhxdaca5cFuk,1062
|
|
97
|
+
codex_lb-0.3.0.dist-info/RECORD,,
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from app.core.auth.refresh import RefreshError, refresh_access_token, should_refresh
|
|
4
|
-
from app.core.balancer import PERMANENT_FAILURE_CODES
|
|
5
|
-
from app.core.crypto import TokenEncryptor
|
|
6
|
-
from app.core.utils.time import utcnow
|
|
7
|
-
from app.db.models import Account, AccountStatus
|
|
8
|
-
from app.modules.accounts.repository import AccountsRepository
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class AuthManager:
|
|
12
|
-
def __init__(self, repo: AccountsRepository) -> None:
|
|
13
|
-
self._repo = repo
|
|
14
|
-
self._encryptor = TokenEncryptor()
|
|
15
|
-
|
|
16
|
-
async def ensure_fresh(self, account: Account, *, force: bool = False) -> Account:
|
|
17
|
-
if force or should_refresh(account.last_refresh):
|
|
18
|
-
return await self.refresh_account(account)
|
|
19
|
-
return account
|
|
20
|
-
|
|
21
|
-
async def refresh_account(self, account: Account) -> Account:
|
|
22
|
-
refresh_token = self._encryptor.decrypt(account.refresh_token_encrypted)
|
|
23
|
-
try:
|
|
24
|
-
result = await refresh_access_token(refresh_token)
|
|
25
|
-
except RefreshError as exc:
|
|
26
|
-
if exc.is_permanent:
|
|
27
|
-
reason = PERMANENT_FAILURE_CODES.get(exc.code, exc.message)
|
|
28
|
-
await self._repo.update_status(account.id, AccountStatus.DEACTIVATED, reason)
|
|
29
|
-
account.status = AccountStatus.DEACTIVATED
|
|
30
|
-
account.deactivation_reason = reason
|
|
31
|
-
raise
|
|
32
|
-
|
|
33
|
-
account.access_token_encrypted = self._encryptor.encrypt(result.access_token)
|
|
34
|
-
account.refresh_token_encrypted = self._encryptor.encrypt(result.refresh_token)
|
|
35
|
-
account.id_token_encrypted = self._encryptor.encrypt(result.id_token)
|
|
36
|
-
account.last_refresh = utcnow()
|
|
37
|
-
if result.plan_type:
|
|
38
|
-
account.plan_type = result.plan_type
|
|
39
|
-
if result.email:
|
|
40
|
-
account.email = result.email
|
|
41
|
-
|
|
42
|
-
await self._repo.update_tokens(
|
|
43
|
-
account.id,
|
|
44
|
-
access_token_encrypted=account.access_token_encrypted,
|
|
45
|
-
refresh_token_encrypted=account.refresh_token_encrypted,
|
|
46
|
-
id_token_encrypted=account.id_token_encrypted,
|
|
47
|
-
last_refresh=account.last_refresh,
|
|
48
|
-
plan_type=account.plan_type,
|
|
49
|
-
email=account.email,
|
|
50
|
-
)
|
|
51
|
-
return account
|