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.
Files changed (56) hide show
  1. app/__init__.py +1 -1
  2. app/core/auth/__init__.py +12 -1
  3. app/core/balancer/logic.py +44 -7
  4. app/core/clients/proxy.py +2 -4
  5. app/core/config/settings.py +4 -1
  6. app/core/plan_types.py +64 -0
  7. app/core/types.py +4 -2
  8. app/core/usage/__init__.py +5 -2
  9. app/core/usage/logs.py +12 -2
  10. app/core/usage/quota.py +64 -0
  11. app/core/usage/types.py +3 -2
  12. app/core/utils/sse.py +6 -2
  13. app/db/migrations/__init__.py +91 -0
  14. app/db/migrations/versions/__init__.py +1 -0
  15. app/db/migrations/versions/add_accounts_chatgpt_account_id.py +29 -0
  16. app/db/migrations/versions/add_accounts_reset_at.py +29 -0
  17. app/db/migrations/versions/add_dashboard_settings.py +31 -0
  18. app/db/migrations/versions/add_request_logs_reasoning_effort.py +21 -0
  19. app/db/migrations/versions/normalize_account_plan_types.py +17 -0
  20. app/db/models.py +33 -0
  21. app/db/session.py +85 -11
  22. app/dependencies.py +27 -9
  23. app/main.py +15 -6
  24. app/modules/accounts/auth_manager.py +121 -0
  25. app/modules/accounts/repository.py +14 -6
  26. app/modules/accounts/service.py +14 -9
  27. app/modules/health/api.py +5 -3
  28. app/modules/health/schemas.py +9 -0
  29. app/modules/oauth/service.py +9 -4
  30. app/modules/proxy/helpers.py +285 -0
  31. app/modules/proxy/load_balancer.py +86 -41
  32. app/modules/proxy/service.py +172 -318
  33. app/modules/proxy/sticky_repository.py +56 -0
  34. app/modules/request_logs/repository.py +6 -3
  35. app/modules/request_logs/schemas.py +2 -0
  36. app/modules/request_logs/service.py +12 -3
  37. app/modules/settings/__init__.py +1 -0
  38. app/modules/settings/api.py +37 -0
  39. app/modules/settings/repository.py +40 -0
  40. app/modules/settings/schemas.py +13 -0
  41. app/modules/settings/service.py +33 -0
  42. app/modules/shared/schemas.py +16 -2
  43. app/modules/usage/schemas.py +1 -0
  44. app/modules/usage/service.py +23 -6
  45. app/modules/{proxy/usage_updater.py → usage/updater.py} +37 -8
  46. app/static/7.css +73 -0
  47. app/static/index.css +33 -4
  48. app/static/index.html +51 -4
  49. app/static/index.js +254 -32
  50. {codex_lb-0.1.5.dist-info → codex_lb-0.3.0.dist-info}/METADATA +2 -2
  51. codex_lb-0.3.0.dist-info/RECORD +97 -0
  52. app/modules/proxy/auth_manager.py +0 -51
  53. codex_lb-0.1.5.dist-info/RECORD +0 -80
  54. {codex_lb-0.1.5.dist-info → codex_lb-0.3.0.dist-info}/WHEEL +0 -0
  55. {codex_lb-0.1.5.dist-info → codex_lb-0.3.0.dist-info}/entry_points.txt +0 -0
  56. {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 PLAN_LABELS = {
78
- plus: "Plus",
79
- team: "Team",
80
- free: "Free",
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: "rate limit",
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 planLabel = (plan) => PLAN_LABELS[plan] || "Unknown";
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
- primaryRemainingPercent ??
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 rawPercent = capacity > 0 ? (value / capacity) * 100 : 0;
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: "Scope: responses",
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 remaining = toNumber(usage.remaining) || 0;
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
- usage.byAccount || [],
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
- detail: `Remaining ${formatPercent(item.remainingPercent)}`,
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
- detail: `${formatPercent(consumedPercent)}`,
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: request.model,
1017
+ model: modelLabel,
877
1018
  status: {
878
1019
  class: requestStatusClass(request.status),
879
1020
  label: requestStatusLabel(request.status),
880
1021
  },
881
- tokens: formatNumber(request.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
- `Selection: ${this.accounts.selectedId || "--"}`,
1681
- `Rotation: ${this.dashboardData.routing?.rotationEnabled ? "enabled" : "disabled"}`,
1682
- `Last sync: ${lastSync}`,
1683
- ]
1902
+ `Selection: ${this.accounts.selectedId || "--"}`,
1903
+ `Rotation: ${this.dashboardData.routing?.rotationEnabled ? "enabled" : "disabled"}`,
1904
+ `Last sync: ${lastSync}`,
1905
+ ]
1684
1906
  : [
1685
- `Last sync: ${lastSync}`,
1686
- `Routing: ${routingLabel(this.dashboardData.routing?.strategy)}`,
1687
- `Backend: ${this.backendPath}`,
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.1.5
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.2
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