codex-lb 0.3.1__py3-none-any.whl → 0.4.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/core/clients/proxy.py +33 -3
- app/core/config/settings.py +1 -0
- app/core/openai/requests.py +21 -3
- app/core/openai/v1_requests.py +148 -0
- app/db/models.py +3 -3
- app/main.py +1 -0
- app/modules/accounts/repository.py +4 -1
- app/modules/proxy/api.py +36 -0
- app/modules/proxy/service.py +29 -0
- app/modules/request_logs/api.py +61 -7
- app/modules/request_logs/repository.py +128 -16
- app/modules/request_logs/schemas.py +11 -2
- app/modules/request_logs/service.py +97 -20
- app/modules/usage/updater.py +58 -26
- app/static/index.css +378 -1
- app/static/index.html +183 -8
- app/static/index.js +308 -13
- {codex_lb-0.3.1.dist-info → codex_lb-0.4.0.dist-info}/METADATA +41 -3
- {codex_lb-0.3.1.dist-info → codex_lb-0.4.0.dist-info}/RECORD +22 -21
- {codex_lb-0.3.1.dist-info → codex_lb-0.4.0.dist-info}/WHEEL +0 -0
- {codex_lb-0.3.1.dist-info → codex_lb-0.4.0.dist-info}/entry_points.txt +0 -0
- {codex_lb-0.3.1.dist-info → codex_lb-0.4.0.dist-info}/licenses/LICENSE +0 -0
app/static/index.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
usageSummary: "/api/usage/summary",
|
|
13
13
|
usageWindow: (window) =>
|
|
14
14
|
`/api/usage/window?window=${encodeURIComponent(window)}`,
|
|
15
|
-
requestLogs:
|
|
15
|
+
requestLogs: "/api/request-logs",
|
|
16
|
+
requestLogOptions: "/api/request-logs/options",
|
|
16
17
|
oauthStart: "/api/oauth/start",
|
|
17
18
|
oauthStatus: "/api/oauth/status",
|
|
18
19
|
oauthComplete: "/api/oauth/complete",
|
|
@@ -79,9 +80,20 @@
|
|
|
79
80
|
ok: "active",
|
|
80
81
|
rate_limit: "limited",
|
|
81
82
|
quota: "exceeded",
|
|
82
|
-
error: "
|
|
83
|
+
error: "error",
|
|
83
84
|
};
|
|
84
85
|
|
|
86
|
+
const MODEL_OPTION_DELIMITER = ":::";
|
|
87
|
+
|
|
88
|
+
const createDefaultRequestFilters = () => ({
|
|
89
|
+
search: "",
|
|
90
|
+
timeframe: "all",
|
|
91
|
+
accountIds: [],
|
|
92
|
+
modelOptions: [],
|
|
93
|
+
statuses: [],
|
|
94
|
+
minCost: "",
|
|
95
|
+
});
|
|
96
|
+
|
|
85
97
|
const KNOWN_PLAN_TYPES = new Set([
|
|
86
98
|
"free",
|
|
87
99
|
"plus",
|
|
@@ -180,11 +192,16 @@
|
|
|
180
192
|
minimumFractionDigits: 2,
|
|
181
193
|
maximumFractionDigits: 2,
|
|
182
194
|
});
|
|
183
|
-
const
|
|
195
|
+
const timeFormatter = new Intl.DateTimeFormat("en-US", {
|
|
184
196
|
hour: "2-digit",
|
|
185
197
|
minute: "2-digit",
|
|
186
198
|
second: "2-digit",
|
|
187
199
|
});
|
|
200
|
+
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
|
201
|
+
year: "numeric",
|
|
202
|
+
month: "2-digit",
|
|
203
|
+
day: "2-digit",
|
|
204
|
+
});
|
|
188
205
|
|
|
189
206
|
const createEmptyDashboardData = () => ({
|
|
190
207
|
lastSyncAt: "",
|
|
@@ -373,9 +390,12 @@
|
|
|
373
390
|
const formatTimeLong = (iso) => {
|
|
374
391
|
const date = parseDate(iso);
|
|
375
392
|
if (!date) {
|
|
376
|
-
return "--";
|
|
393
|
+
return { time: "--", date: "--" };
|
|
377
394
|
}
|
|
378
|
-
return
|
|
395
|
+
return {
|
|
396
|
+
time: timeFormatter.format(date),
|
|
397
|
+
date: dateFormatter.format(date),
|
|
398
|
+
};
|
|
379
399
|
};
|
|
380
400
|
|
|
381
401
|
const formatRelative = (ms) => {
|
|
@@ -768,7 +788,7 @@
|
|
|
768
788
|
typeof windowMinutes === "number" && windowMinutes > 0
|
|
769
789
|
? windowMinutes / 60
|
|
770
790
|
: 24 * 7;
|
|
771
|
-
return
|
|
791
|
+
return numeric / hours;
|
|
772
792
|
};
|
|
773
793
|
|
|
774
794
|
const countByStatus = (accounts) =>
|
|
@@ -1055,6 +1075,9 @@
|
|
|
1055
1075
|
const rawError = request.errorMessage || request.errorCode || "";
|
|
1056
1076
|
const accountLabel = formatAccountLabel(request.accountId, accounts);
|
|
1057
1077
|
const modelLabel = formatModelLabel(request.model, request.reasoningEffort);
|
|
1078
|
+
const totalTokens = formatCompactNumber(request.tokens);
|
|
1079
|
+
const cachedInputTokens = toNumber(request.cachedInputTokens);
|
|
1080
|
+
const cachedTokens = cachedInputTokens > 0 ? formatCompactNumber(cachedInputTokens) : null;
|
|
1058
1081
|
return {
|
|
1059
1082
|
key: `${request.requestId}-${request.timestamp}`,
|
|
1060
1083
|
requestId: request.requestId,
|
|
@@ -1065,10 +1088,16 @@
|
|
|
1065
1088
|
class: requestStatusClass(request.status),
|
|
1066
1089
|
label: requestStatusLabel(request.status),
|
|
1067
1090
|
},
|
|
1068
|
-
tokens:
|
|
1091
|
+
tokens: {
|
|
1092
|
+
total: totalTokens,
|
|
1093
|
+
cached: cachedTokens,
|
|
1094
|
+
},
|
|
1095
|
+
tokensTooltip: formatTokensWithCached(request.tokens, request.cachedInputTokens),
|
|
1069
1096
|
cost: formatCurrency(request.cost),
|
|
1070
1097
|
error: rawError ? truncateText(rawError, 80) : "--",
|
|
1071
1098
|
errorTitle: rawError,
|
|
1099
|
+
isTruncated: rawError.length > 20,
|
|
1100
|
+
isErrorPlaceholder: !rawError,
|
|
1072
1101
|
};
|
|
1073
1102
|
});
|
|
1074
1103
|
|
|
@@ -1237,8 +1266,107 @@
|
|
|
1237
1266
|
const fetchUsageWindow = async (window) =>
|
|
1238
1267
|
fetchJson(API_ENDPOINTS.usageWindow(window), `usage window (${window})`);
|
|
1239
1268
|
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1269
|
+
const parseMinCostUsd = (value) => {
|
|
1270
|
+
if (value === null || value === undefined) {
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
const asString = String(value).trim();
|
|
1274
|
+
if (!asString) {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
const numeric = Number(asString);
|
|
1278
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
const buildSinceIsoFromTimeframe = (timeframe) => {
|
|
1282
|
+
const now = Date.now();
|
|
1283
|
+
if (timeframe === "1h") {
|
|
1284
|
+
return new Date(now - 60 * 60 * 1000).toISOString();
|
|
1285
|
+
}
|
|
1286
|
+
if (timeframe === "24h") {
|
|
1287
|
+
return new Date(now - 24 * 60 * 60 * 1000).toISOString();
|
|
1288
|
+
}
|
|
1289
|
+
if (timeframe === "7d") {
|
|
1290
|
+
return new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
1291
|
+
}
|
|
1292
|
+
return null;
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
const buildRequestLogsQueryParams = ({ filters, pagination }) => {
|
|
1296
|
+
const params = {
|
|
1297
|
+
limit: pagination?.limit,
|
|
1298
|
+
offset: pagination?.offset,
|
|
1299
|
+
};
|
|
1300
|
+
if (filters?.search) {
|
|
1301
|
+
params.search = filters.search;
|
|
1302
|
+
}
|
|
1303
|
+
if (Array.isArray(filters?.accountIds) && filters.accountIds.length) {
|
|
1304
|
+
params.accountId = filters.accountIds.filter(Boolean);
|
|
1305
|
+
}
|
|
1306
|
+
if (Array.isArray(filters?.modelOptions) && filters.modelOptions.length) {
|
|
1307
|
+
params.modelOption = filters.modelOptions.filter(Boolean);
|
|
1308
|
+
}
|
|
1309
|
+
if (Array.isArray(filters?.statuses) && filters.statuses.length) {
|
|
1310
|
+
params.status = filters.statuses.filter(Boolean);
|
|
1311
|
+
}
|
|
1312
|
+
const since = buildSinceIsoFromTimeframe(filters?.timeframe);
|
|
1313
|
+
if (since) {
|
|
1314
|
+
params.since = since;
|
|
1315
|
+
}
|
|
1316
|
+
return params;
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
const applyClientSideRequestFilters = (requests, filters) => {
|
|
1320
|
+
const minCostUsd = parseMinCostUsd(filters?.minCost);
|
|
1321
|
+
if (minCostUsd === null) {
|
|
1322
|
+
return requests;
|
|
1323
|
+
}
|
|
1324
|
+
return (requests || []).filter((entry) => (entry.cost || 0) >= minCostUsd);
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
const buildQueryString = (params) => {
|
|
1328
|
+
const searchParams = new URLSearchParams();
|
|
1329
|
+
if (!params || typeof params !== "object") {
|
|
1330
|
+
return "";
|
|
1331
|
+
}
|
|
1332
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1333
|
+
if (value === null || value === undefined) {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (Array.isArray(value)) {
|
|
1337
|
+
const filtered = value
|
|
1338
|
+
.map((item) => String(item ?? "").trim())
|
|
1339
|
+
.filter(Boolean);
|
|
1340
|
+
for (const item of filtered) {
|
|
1341
|
+
searchParams.append(key, item);
|
|
1342
|
+
}
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
searchParams.append(key, String(value));
|
|
1349
|
+
}
|
|
1350
|
+
return searchParams.toString();
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
const fetchRequestLogs = async (params) => {
|
|
1354
|
+
const query = buildQueryString(params);
|
|
1355
|
+
return fetchJson(
|
|
1356
|
+
query ? `${API_ENDPOINTS.requestLogs}?${query}` : API_ENDPOINTS.requestLogs,
|
|
1357
|
+
"request logs",
|
|
1358
|
+
);
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
const fetchRequestLogOptions = async (params) => {
|
|
1362
|
+
const query = buildQueryString(params);
|
|
1363
|
+
return fetchJson(
|
|
1364
|
+
query
|
|
1365
|
+
? `${API_ENDPOINTS.requestLogOptions}?${query}`
|
|
1366
|
+
: API_ENDPOINTS.requestLogOptions,
|
|
1367
|
+
"request log options",
|
|
1368
|
+
);
|
|
1369
|
+
};
|
|
1242
1370
|
|
|
1243
1371
|
const normalizeSettingsPayload = (payload) => ({
|
|
1244
1372
|
stickyThreadsEnabled: Boolean(payload?.stickyThreadsEnabled),
|
|
@@ -1258,6 +1386,24 @@
|
|
|
1258
1386
|
ui: createUiConfig(),
|
|
1259
1387
|
dashboardData: createEmptyDashboardData(),
|
|
1260
1388
|
dashboard: createEmptyDashboardView(),
|
|
1389
|
+
|
|
1390
|
+
filtersDraft: createDefaultRequestFilters(),
|
|
1391
|
+
filtersApplied: createDefaultRequestFilters(),
|
|
1392
|
+
pagination: {
|
|
1393
|
+
limit: 25,
|
|
1394
|
+
offset: 0,
|
|
1395
|
+
},
|
|
1396
|
+
recentRequestsState: {
|
|
1397
|
+
isLoading: false,
|
|
1398
|
+
error: "",
|
|
1399
|
+
},
|
|
1400
|
+
requestLogOptions: {
|
|
1401
|
+
accountIds: [],
|
|
1402
|
+
modelOptions: [],
|
|
1403
|
+
isLoading: false,
|
|
1404
|
+
error: "",
|
|
1405
|
+
},
|
|
1406
|
+
|
|
1261
1407
|
settings: {
|
|
1262
1408
|
stickyThreadsEnabled: false,
|
|
1263
1409
|
preferEarlierResetAccounts: false,
|
|
@@ -1305,12 +1451,19 @@
|
|
|
1305
1451
|
isLoading: true,
|
|
1306
1452
|
hasInitialized: false,
|
|
1307
1453
|
refreshPromise: null,
|
|
1454
|
+
|
|
1308
1455
|
async init() {
|
|
1309
1456
|
if (this.hasInitialized) {
|
|
1310
1457
|
return;
|
|
1311
1458
|
}
|
|
1312
1459
|
this.hasInitialized = true;
|
|
1313
1460
|
this.view = getViewFromPath(window.location.pathname);
|
|
1461
|
+
|
|
1462
|
+
this.$watch("pagination.limit", () => {
|
|
1463
|
+
this.pagination.offset = 0;
|
|
1464
|
+
this.refreshRequests();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1314
1467
|
await this.loadData();
|
|
1315
1468
|
this.syncTitle();
|
|
1316
1469
|
this.syncUrl(true);
|
|
@@ -1329,6 +1482,115 @@
|
|
|
1329
1482
|
}
|
|
1330
1483
|
});
|
|
1331
1484
|
},
|
|
1485
|
+
|
|
1486
|
+
async refreshRequests() {
|
|
1487
|
+
try {
|
|
1488
|
+
this.recentRequestsState.isLoading = true;
|
|
1489
|
+
this.recentRequestsState.error = "";
|
|
1490
|
+
const params = buildRequestLogsQueryParams({
|
|
1491
|
+
filters: this.filtersApplied,
|
|
1492
|
+
pagination: this.pagination,
|
|
1493
|
+
});
|
|
1494
|
+
const requestsData = await fetchRequestLogs(params);
|
|
1495
|
+
const normalized = normalizeRequestLogsPayload(requestsData);
|
|
1496
|
+
const requests = applyClientSideRequestFilters(
|
|
1497
|
+
normalized,
|
|
1498
|
+
this.filtersApplied,
|
|
1499
|
+
);
|
|
1500
|
+
|
|
1501
|
+
this.dashboardData.recentRequests = requests;
|
|
1502
|
+
|
|
1503
|
+
this.dashboard = buildDashboardView(this);
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
this.recentRequestsState.error =
|
|
1506
|
+
err?.message || "Failed to refresh requests.";
|
|
1507
|
+
console.error("Failed to refresh requests:", err);
|
|
1508
|
+
} finally {
|
|
1509
|
+
this.recentRequestsState.isLoading = false;
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
|
|
1513
|
+
applyFilters() {
|
|
1514
|
+
this.pagination.offset = 0;
|
|
1515
|
+
const accountIds = Array.isArray(this.filtersDraft.accountIds)
|
|
1516
|
+
? [...new Set(this.filtersDraft.accountIds.map(String).filter(Boolean))]
|
|
1517
|
+
: [];
|
|
1518
|
+
const modelOptions = Array.isArray(this.filtersDraft.modelOptions)
|
|
1519
|
+
? [...new Set(this.filtersDraft.modelOptions.map(String).filter(Boolean))]
|
|
1520
|
+
: [];
|
|
1521
|
+
const statuses = Array.isArray(this.filtersDraft.statuses)
|
|
1522
|
+
? [...new Set(this.filtersDraft.statuses.map(String).filter(Boolean))]
|
|
1523
|
+
: [];
|
|
1524
|
+
this.filtersApplied = {
|
|
1525
|
+
...createDefaultRequestFilters(),
|
|
1526
|
+
...this.filtersDraft,
|
|
1527
|
+
accountIds,
|
|
1528
|
+
modelOptions,
|
|
1529
|
+
statuses,
|
|
1530
|
+
};
|
|
1531
|
+
this.refreshRequests();
|
|
1532
|
+
},
|
|
1533
|
+
|
|
1534
|
+
resetFilters() {
|
|
1535
|
+
this.filtersDraft = createDefaultRequestFilters();
|
|
1536
|
+
this.applyFilters();
|
|
1537
|
+
},
|
|
1538
|
+
|
|
1539
|
+
accountFilterLabel(accountId) {
|
|
1540
|
+
return formatAccountLabel(accountId, this.accounts.rows);
|
|
1541
|
+
},
|
|
1542
|
+
|
|
1543
|
+
modelOptionValue(option) {
|
|
1544
|
+
const model = String(option?.model || "").trim();
|
|
1545
|
+
const effort = String(option?.reasoningEffort || "").trim();
|
|
1546
|
+
return `${model}${MODEL_OPTION_DELIMITER}${effort}`;
|
|
1547
|
+
},
|
|
1548
|
+
|
|
1549
|
+
modelOptionLabel(option) {
|
|
1550
|
+
return formatModelLabel(option?.model, option?.reasoningEffort);
|
|
1551
|
+
},
|
|
1552
|
+
|
|
1553
|
+
toggleMultiSelectValue(listKey, rawValue) {
|
|
1554
|
+
const value = String(rawValue ?? "").trim();
|
|
1555
|
+
if (!value) {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const current = Array.isArray(this.filtersDraft[listKey])
|
|
1559
|
+
? [...this.filtersDraft[listKey]]
|
|
1560
|
+
: [];
|
|
1561
|
+
const index = current.indexOf(value);
|
|
1562
|
+
if (index >= 0) {
|
|
1563
|
+
current.splice(index, 1);
|
|
1564
|
+
} else {
|
|
1565
|
+
current.push(value);
|
|
1566
|
+
}
|
|
1567
|
+
this.filtersDraft = { ...this.filtersDraft, [listKey]: current };
|
|
1568
|
+
},
|
|
1569
|
+
|
|
1570
|
+
multiSelectSummary(values, emptyLabel, singularLabel, pluralLabel) {
|
|
1571
|
+
const list = Array.isArray(values)
|
|
1572
|
+
? values.map((value) => String(value ?? "").trim()).filter(Boolean)
|
|
1573
|
+
: [];
|
|
1574
|
+
if (list.length === 0) {
|
|
1575
|
+
return emptyLabel;
|
|
1576
|
+
}
|
|
1577
|
+
if (list.length === 1) {
|
|
1578
|
+
return `1 ${singularLabel}`;
|
|
1579
|
+
}
|
|
1580
|
+
const plural = pluralLabel ? String(pluralLabel) : `${singularLabel}s`;
|
|
1581
|
+
return `${list.length} ${plural}`;
|
|
1582
|
+
},
|
|
1583
|
+
|
|
1584
|
+
timeframeLabel(value) {
|
|
1585
|
+
const labels = {
|
|
1586
|
+
all: "All time",
|
|
1587
|
+
"1h": "Last 1h",
|
|
1588
|
+
"24h": "Last 24h",
|
|
1589
|
+
"7d": "Last 7d",
|
|
1590
|
+
};
|
|
1591
|
+
return labels[value] || "All time";
|
|
1592
|
+
},
|
|
1593
|
+
|
|
1332
1594
|
async loadData() {
|
|
1333
1595
|
try {
|
|
1334
1596
|
await this.refreshAll({ silent: true });
|
|
@@ -1351,22 +1613,32 @@
|
|
|
1351
1613
|
return this.refreshPromise;
|
|
1352
1614
|
}
|
|
1353
1615
|
const { preferredId, silent = false } = options;
|
|
1616
|
+
this.requestLogOptions.isLoading = true;
|
|
1617
|
+
this.requestLogOptions.error = "";
|
|
1354
1618
|
this.refreshPromise = (async () => {
|
|
1619
|
+
const params = buildRequestLogsQueryParams({
|
|
1620
|
+
filters: this.filtersApplied,
|
|
1621
|
+
pagination: this.pagination,
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1355
1624
|
const [
|
|
1356
1625
|
accountsResult,
|
|
1357
1626
|
summaryResult,
|
|
1358
1627
|
primaryResult,
|
|
1359
1628
|
secondaryResult,
|
|
1360
1629
|
requestLogsResult,
|
|
1630
|
+
requestLogOptionsResult,
|
|
1361
1631
|
settingsResult,
|
|
1362
1632
|
] = await Promise.allSettled([
|
|
1363
1633
|
fetchAccounts(),
|
|
1364
1634
|
fetchUsageSummary(),
|
|
1365
1635
|
fetchUsageWindow("primary"),
|
|
1366
1636
|
fetchUsageWindow("secondary"),
|
|
1367
|
-
fetchRequestLogs(
|
|
1637
|
+
fetchRequestLogs(params),
|
|
1638
|
+
fetchRequestLogOptions({}),
|
|
1368
1639
|
fetchSettings(),
|
|
1369
1640
|
]);
|
|
1641
|
+
|
|
1370
1642
|
const errors = [];
|
|
1371
1643
|
if (accountsResult.status !== "fulfilled") {
|
|
1372
1644
|
throw accountsResult.reason;
|
|
@@ -1398,6 +1670,21 @@
|
|
|
1398
1670
|
errors.push(requestLogsResult.reason);
|
|
1399
1671
|
}
|
|
1400
1672
|
|
|
1673
|
+
if (requestLogOptionsResult.status === "fulfilled") {
|
|
1674
|
+
const payload = requestLogOptionsResult.value;
|
|
1675
|
+
this.requestLogOptions.accountIds = Array.isArray(payload?.accountIds)
|
|
1676
|
+
? payload.accountIds
|
|
1677
|
+
: [];
|
|
1678
|
+
this.requestLogOptions.modelOptions = Array.isArray(payload?.modelOptions)
|
|
1679
|
+
? payload.modelOptions
|
|
1680
|
+
: [];
|
|
1681
|
+
} else {
|
|
1682
|
+
this.requestLogOptions.error =
|
|
1683
|
+
requestLogOptionsResult.reason?.message ||
|
|
1684
|
+
"Failed to load request log options.";
|
|
1685
|
+
errors.push(requestLogOptionsResult.reason);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1401
1688
|
const settings =
|
|
1402
1689
|
settingsResult.status === "fulfilled" ? settingsResult.value : null;
|
|
1403
1690
|
if (settingsResult.status === "rejected") {
|
|
@@ -1415,7 +1702,10 @@
|
|
|
1415
1702
|
summary,
|
|
1416
1703
|
primaryUsage,
|
|
1417
1704
|
secondaryUsage,
|
|
1418
|
-
requestLogs
|
|
1705
|
+
requestLogs: applyClientSideRequestFilters(
|
|
1706
|
+
requestLogs,
|
|
1707
|
+
this.filtersApplied,
|
|
1708
|
+
),
|
|
1419
1709
|
settings,
|
|
1420
1710
|
},
|
|
1421
1711
|
preferredId,
|
|
@@ -1437,6 +1727,7 @@
|
|
|
1437
1727
|
await this.refreshPromise;
|
|
1438
1728
|
} finally {
|
|
1439
1729
|
this.refreshPromise = null;
|
|
1730
|
+
this.requestLogOptions.isLoading = false;
|
|
1440
1731
|
}
|
|
1441
1732
|
},
|
|
1442
1733
|
applyData(data, preferredId) {
|
|
@@ -1960,15 +2251,19 @@
|
|
|
1960
2251
|
},
|
|
1961
2252
|
get statusItems() {
|
|
1962
2253
|
const lastSync = formatTimeLong(this.dashboardData.lastSyncAt);
|
|
2254
|
+
const lastSyncLabel =
|
|
2255
|
+
lastSync && lastSync.time && lastSync.time !== "--"
|
|
2256
|
+
? `${lastSync.time} · ${lastSync.date}`
|
|
2257
|
+
: "--";
|
|
1963
2258
|
const items =
|
|
1964
2259
|
this.view === "accounts"
|
|
1965
2260
|
? [
|
|
1966
2261
|
`Selection: ${this.accounts.selectedId || "--"}`,
|
|
1967
2262
|
`Rotation: ${this.dashboardData.routing?.rotationEnabled ? "enabled" : "disabled"}`,
|
|
1968
|
-
`Last sync: ${
|
|
2263
|
+
`Last sync: ${lastSyncLabel}`,
|
|
1969
2264
|
]
|
|
1970
2265
|
: [
|
|
1971
|
-
`Last sync: ${
|
|
2266
|
+
`Last sync: ${lastSyncLabel}`,
|
|
1972
2267
|
`Routing: ${routingLabel(this.dashboardData.routing?.strategy)}`,
|
|
1973
2268
|
`Backend: ${this.backendPath}`,
|
|
1974
2269
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-lb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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>
|
|
@@ -51,10 +51,22 @@ Requires-Dist: python-multipart>=0.0.21
|
|
|
51
51
|
Requires-Dist: sqlalchemy>=2.0.45
|
|
52
52
|
Description-Content-Type: text/markdown
|
|
53
53
|
|
|
54
|
+
<!--
|
|
55
|
+
About
|
|
56
|
+
Codex/ChatGPT account load balancer & proxy with usage tracking, dashboard, and OpenCode-compatible endpoints
|
|
57
|
+
|
|
58
|
+
Topics
|
|
59
|
+
python oauth sqlalchemy dashboard load-balancer openai rate-limit api-proxy codex fastapi usage-tracking chatgpt opencode
|
|
60
|
+
|
|
61
|
+
Resources
|
|
62
|
+
-->
|
|
63
|
+
|
|
54
64
|
# codex-lb
|
|
55
65
|
|
|
56
66
|
Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
|
|
57
67
|
|
|
68
|
+
## Screenshots
|
|
69
|
+
|
|
58
70
|
### Main Dashboard View
|
|
59
71
|
|
|
60
72
|

|
|
@@ -82,8 +94,6 @@ uvx codex-lb
|
|
|
82
94
|
|
|
83
95
|
Open [localhost:2455](http://localhost:2455) → Add account → Done.
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
87
97
|
## Codex CLI & Extension Setup
|
|
88
98
|
|
|
89
99
|
Add to `~/.codex/config.toml`:
|
|
@@ -101,6 +111,32 @@ chatgpt_base_url = "http://127.0.0.1:2455"
|
|
|
101
111
|
requires_openai_auth = true # Required: enables model selection in Codex IDE extension
|
|
102
112
|
```
|
|
103
113
|
|
|
114
|
+
## OpenCode Setup
|
|
115
|
+
|
|
116
|
+
Run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
opencode auth login
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Then select `OpenAI` -> `Manually enter API Key` and enter any value.
|
|
123
|
+
|
|
124
|
+
Add the following to `~/.config/opencode/opencode.json`:
|
|
125
|
+
|
|
126
|
+
```jsonc
|
|
127
|
+
{
|
|
128
|
+
...
|
|
129
|
+
"provider": {
|
|
130
|
+
"openai": {
|
|
131
|
+
"options": {
|
|
132
|
+
"baseURL": "http://127.0.0.1:2455/v1"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
...
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
104
140
|
## Data
|
|
105
141
|
|
|
106
142
|
All data stored in `~/.codex-lb/`:
|
|
@@ -122,6 +158,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
122
158
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Soju06"><img src="https://avatars.githubusercontent.com/u/34199905?v=4?s=100" width="100px;" alt="Soju06"/><br /><sub><b>Soju06</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Soju06" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=Soju06" title="Tests">⚠️</a> <a href="#maintenance-Soju06" title="Maintenance">🚧</a> <a href="#infra-Soju06" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
123
159
|
<td align="center" valign="top" width="14.28%"><a href="http://jonas.kamsker.at/"><img src="https://avatars.githubusercontent.com/u/11245306?v=4?s=100" width="100px;" alt="Jonas Kamsker"/><br /><sub><b>Jonas Kamsker</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=JKamsker" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AJKamsker" title="Bug reports">🐛</a> <a href="#maintenance-JKamsker" title="Maintenance">🚧</a></td>
|
|
124
160
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Quack6765"><img src="https://avatars.githubusercontent.com/u/5446230?v=4?s=100" width="100px;" alt="Quack"/><br /><sub><b>Quack</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Quack6765" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AQuack6765" title="Bug reports">🐛</a> <a href="#maintenance-Quack6765" title="Maintenance">🚧</a> <a href="#design-Quack6765" title="Design">🎨</a></td>
|
|
161
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hhsw2015"><img src="https://avatars.githubusercontent.com/u/103614420?v=4?s=100" width="100px;" alt="Jill Kok, San Mou"/><br /><sub><b>Jill Kok, San Mou</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Tests">⚠️</a></td>
|
|
162
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pcy06"><img src="https://avatars.githubusercontent.com/u/44970486?v=4?s=100" width="100px;" alt="PARK CHANYOUNG"/><br /><sub><b>PARK CHANYOUNG</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=pcy06" title="Documentation">📖</a></td>
|
|
125
163
|
</tr>
|
|
126
164
|
</tbody>
|
|
127
165
|
</table>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
app/__init__.py,sha256=uqZSnn_VEL8TIUxsYqdf4zA2ByJYfjA06fArVAzrHFo,89
|
|
2
2
|
app/cli.py,sha256=gkIAkYOT9SbQjUDnVmwhVKZeKjL3YJCMrOjFINwBx54,544
|
|
3
3
|
app/dependencies.py,sha256=kfB_TxeZve_cnBxhOHZezKwkOwTNFwoHzNCJDUkGJD8,4377
|
|
4
|
-
app/main.py,sha256=
|
|
4
|
+
app/main.py,sha256=7J8mkp_nDqYTo68gu1OwbwsZrVmVG1XVYsmHGXXQm-0,4709
|
|
5
5
|
app/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
app/core/crypto.py,sha256=zUz2GVqXigzXB5zX2Diq2cR41Kl37bBqxJiZmWGiIcY,1145
|
|
7
7
|
app/core/errors.py,sha256=go4Q5vv6_Rt8ZS230Mp436yCViEKu_xICylGF0gvGJg,1805
|
|
@@ -16,14 +16,15 @@ app/core/balancer/types.py,sha256=gDgjlTy-NH3YhHYl2-YYpIabnckN9Q8-4cRy6S1u0K4,19
|
|
|
16
16
|
app/core/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
app/core/clients/http.py,sha256=yfFwsaIZNbSmNtyV02WnMKn00JdoNNSlSIx5xB3QY4o,987
|
|
18
18
|
app/core/clients/oauth.py,sha256=XgAzQVAMudBUCQ9nGKUC9N7zagSiawBdhIVmxf9HHwQ,11832
|
|
19
|
-
app/core/clients/proxy.py,sha256=
|
|
19
|
+
app/core/clients/proxy.py,sha256=lplog7s1pg1OJb8ZEM1wGbgM_omTUWLkBRZ4McLGevY,10367
|
|
20
20
|
app/core/clients/usage.py,sha256=kG7TXqmy8IX9m4wJx5fOGDB2hqunivOU6o2xfoXCGy4,4800
|
|
21
21
|
app/core/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
app/core/config/settings.py,sha256=
|
|
22
|
+
app/core/config/settings.py,sha256=hI4WwQnEajV36e5UIOJZWiF-ghXxVzfPC8BjMMDOVU0,2692
|
|
23
23
|
app/core/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
app/core/openai/models.py,sha256=SzVPqmp9IDbJTW6gLGHeLxrpAmFehJPSC4fVsqzEmQA,3344
|
|
25
25
|
app/core/openai/parsing.py,sha256=VuE1OyPAv1umrSbkzqa6dcjPYfk00isOVB3O0xUPLBw,1537
|
|
26
|
-
app/core/openai/requests.py,sha256=
|
|
26
|
+
app/core/openai/requests.py,sha256=p-by1zrNAUVNJMjTf3KHT9g9dZs1MmVrqxVpIT9-X80,2252
|
|
27
|
+
app/core/openai/v1_requests.py,sha256=4WIGXwmkXSgoSFtseJIIYiuAx7WT3gqQWYlZhKnOFlc,5612
|
|
27
28
|
app/core/usage/__init__.py,sha256=8SBpiClJR2wl653Cj1DTvmVUvi2jzU6kKXJtq91ofmE,5726
|
|
28
29
|
app/core/usage/logs.py,sha256=8TrE9nccEjIJslMfAk8AJLmX_Zzeksu8qMTvrGwePx0,2129
|
|
29
30
|
app/core/usage/models.py,sha256=FtBQx4Rb7jpwcqxmGXxg7RTVV17LK1QOSWaJIkaaNoQ,878
|
|
@@ -36,7 +37,7 @@ app/core/utils/retry.py,sha256=UmBap1Wh-CBT7r4fHzVb_PI9-LR9-HjUtDzRnhRjP2U,822
|
|
|
36
37
|
app/core/utils/sse.py,sha256=DJMOU4vW5Ir_4WeL5t5t7i33aRMnqVPU0eQvGn4sBv8,537
|
|
37
38
|
app/core/utils/time.py,sha256=B6FfSe43Eq_puE6eourly1X3gajyihK2VOAwJ8M3wyI,497
|
|
38
39
|
app/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
app/db/models.py,sha256=
|
|
40
|
+
app/db/models.py,sha256=vjQ2C3l8J1zlsRz9gVzM4fbcX43NGrj51cfCsM4EjNk,5304
|
|
40
41
|
app/db/session.py,sha256=T_UdGcqzn5YXo-fuRW8s5xJQvOuJsV4eTSZE0_V2C44,3910
|
|
41
42
|
app/db/migrations/__init__.py,sha256=hRZVLgAQfP-okuhIiKkQtwfnPtBvYXcLJTqg0Yex1Vc,2633
|
|
42
43
|
app/db/migrations/versions/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
@@ -49,7 +50,7 @@ app/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
49
50
|
app/modules/accounts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
51
|
app/modules/accounts/api.py,sha256=rSkYrg3_p_JE1kHZ4xXa5lBFTdT40v1oNAmlTDGOguY,2807
|
|
51
52
|
app/modules/accounts/auth_manager.py,sha256=YUKdeHbszRLYZ4uRQzvk6ICk2cB73LjlJMEiWphBWWw,4682
|
|
52
|
-
app/modules/accounts/repository.py,sha256=
|
|
53
|
+
app/modules/accounts/repository.py,sha256=hu7yQsGfPmHOBy7BM3aKlrZuNr4_qPjDDXcAneX1K2c,3730
|
|
53
54
|
app/modules/accounts/schemas.py,sha256=gtlbPg5uxM3t_V5JxCL6eP-UaU6TSE0UoX2yIpxM_a0,1659
|
|
54
55
|
app/modules/accounts/service.py,sha256=M3_SRD21yp0YN820IHdqNIlZsynNsSUN6TO6RcESDeI,8973
|
|
55
56
|
app/modules/health/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -61,18 +62,18 @@ app/modules/oauth/schemas.py,sha256=sdDKP7u9bO87lcZXjK7uSokurHPS22nN2p_jkw9iEBc,
|
|
|
61
62
|
app/modules/oauth/service.py,sha256=fEzUtoq1g-20NgWZlB_maA2XB-scMa0HJV24SxZPnqQ,12908
|
|
62
63
|
app/modules/oauth/templates/oauth_success.html,sha256=YNSGUIozcZEJQjpFtM2sgF4n8jqfbmx8LRwdXTraym4,3799
|
|
63
64
|
app/modules/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
-
app/modules/proxy/api.py,sha256=
|
|
65
|
+
app/modules/proxy/api.py,sha256=HhJPBa-I6EL0N4jqkDtP1ZrTHNq5yt2Kzn0D0LJK7QQ,4032
|
|
65
66
|
app/modules/proxy/helpers.py,sha256=-XVTNTpkDvJ3KfsuGHztfUVBGBFm2H-AaNTNH2dNe6o,8739
|
|
66
67
|
app/modules/proxy/load_balancer.py,sha256=Forib096qbWud11aKw9TRbBN1bIQjOmE3ZYY62vhVYk,9831
|
|
67
68
|
app/modules/proxy/schemas.py,sha256=55pXtUCl2R_93kAPOJJ7Ji4Jn3qVu10vq2KSCCkNdp4,2748
|
|
68
|
-
app/modules/proxy/service.py,sha256=
|
|
69
|
+
app/modules/proxy/service.py,sha256=4lnOoqvCNiRhCVOcshnKslbiH4O35iYn1S3CykJkhMA,24578
|
|
69
70
|
app/modules/proxy/sticky_repository.py,sha256=peFaAnaCVr064W-Sh0Kjvz-MuPbBfrh86RLQOGJ6qqs,2177
|
|
70
71
|
app/modules/proxy/types.py,sha256=iqEyoO8vGr8N5oEzUSvVWCai7UZbJAU62IvO7JNS9qs,927
|
|
71
72
|
app/modules/request_logs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
|
-
app/modules/request_logs/api.py,sha256=
|
|
73
|
-
app/modules/request_logs/repository.py,sha256=
|
|
74
|
-
app/modules/request_logs/schemas.py,sha256=
|
|
75
|
-
app/modules/request_logs/service.py,sha256=
|
|
73
|
+
app/modules/request_logs/api.py,sha256=NkLHo8Yuy4S6m00yUIOmzcag9scEaSG2QL7CDRYfyUE,3158
|
|
74
|
+
app/modules/request_logs/repository.py,sha256=e0-z6-ZfJpMMEShqpK-k23C5y1LnTxawpfMaaiRco40,8560
|
|
75
|
+
app/modules/request_logs/schemas.py,sha256=MFS_MW0mW3cGyIulxJN81dJ3SS2bt0tFE67-DzYX1Pg,944
|
|
76
|
+
app/modules/request_logs/service.py,sha256=ruZz5QAKYSrJbYpImZzU2va1sLFfamWL8dOhqU-GG0A,5485
|
|
76
77
|
app/modules/settings/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
77
78
|
app/modules/settings/api.py,sha256=dbnvKUF4lgX5ipxOik4SJBeJ6hctJKe3lcT2Uf0F5BQ,1461
|
|
78
79
|
app/modules/settings/repository.py,sha256=r-GjuURPcftCx5HnFOZKFpv34g9bvy0m53owVCc3tpI,1204
|
|
@@ -85,12 +86,12 @@ app/modules/usage/api.py,sha256=jpPc2VSjmiP6DjRZvotHCh_trLINOngSyTdS7MY9fgg,1108
|
|
|
85
86
|
app/modules/usage/repository.py,sha256=DtJI4kgajW7YUJ0JKJjdNCPBXT_fdBwqDoepi9aznyA,4791
|
|
86
87
|
app/modules/usage/schemas.py,sha256=eCgunQOvQeYEqv9IecjunONPVLpg2MPn_YGzsnBTcpQ,1633
|
|
87
88
|
app/modules/usage/service.py,sha256=8-XX8m4TgQteum-a53l0DS-EL2NnC4r9artehfFltvM,10315
|
|
88
|
-
app/modules/usage/updater.py,sha256=
|
|
89
|
-
app/static/index.css,sha256=
|
|
90
|
-
app/static/index.html,sha256=
|
|
91
|
-
app/static/index.js,sha256=
|
|
92
|
-
codex_lb-0.
|
|
93
|
-
codex_lb-0.
|
|
94
|
-
codex_lb-0.
|
|
95
|
-
codex_lb-0.
|
|
96
|
-
codex_lb-0.
|
|
89
|
+
app/modules/usage/updater.py,sha256=TUFLIjIe8c0m06bnO6iumzAyFuMED3B1ry3Ty5o8JRo,7821
|
|
90
|
+
app/static/index.css,sha256=4EcLWTHAkhbqldi9fE1Y_bQCtmJuXUgEs0JHV-KmV9w,31757
|
|
91
|
+
app/static/index.html,sha256=g1U2AlbvGChev5KIyPoVI5TJ3dnvEOqoAot1tENvQOs,39799
|
|
92
|
+
app/static/index.js,sha256=q-IXWQURYksd-i68yXwyqBazs-AYiz3Tvk5t_B6NbSY,69750
|
|
93
|
+
codex_lb-0.4.0.dist-info/METADATA,sha256=VQXBteRPg3SxSUeq9oAQW3x3VRsTsk2KWHxNv-IfKZE,7244
|
|
94
|
+
codex_lb-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
95
|
+
codex_lb-0.4.0.dist-info/entry_points.txt,sha256=SEa5T6Uz2Fhy574No6Y0XyGmYi3PXLrhu2xStJTqyI8,42
|
|
96
|
+
codex_lb-0.4.0.dist-info/licenses/LICENSE,sha256=cHPibxiL0TXwrUX_kNY6ym544EX1UCzKhxdaca5cFuk,1062
|
|
97
|
+
codex_lb-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|