codex-usage-tracking 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.
- codex_usage_tracker/__init__.py +7 -0
- codex_usage_tracker/__main__.py +6 -0
- codex_usage_tracker/allowance.py +759 -0
- codex_usage_tracker/api_payloads.py +90 -0
- codex_usage_tracker/cli.py +1326 -0
- codex_usage_tracker/context.py +410 -0
- codex_usage_tracker/costing.py +176 -0
- codex_usage_tracker/dashboard.py +389 -0
- codex_usage_tracker/diagnostics.py +624 -0
- codex_usage_tracker/formatting.py +225 -0
- codex_usage_tracker/json_contracts.py +350 -0
- codex_usage_tracker/mcp_server.py +371 -0
- codex_usage_tracker/models.py +92 -0
- codex_usage_tracker/parser.py +491 -0
- codex_usage_tracker/paths.py +18 -0
- codex_usage_tracker/plugin_data/__init__.py +1 -0
- codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
- codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
- codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
- codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
- codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
- codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
- codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
- codex_usage_tracker/plugin_installer.py +312 -0
- codex_usage_tracker/pricing.py +57 -0
- codex_usage_tracker/pricing_config.py +223 -0
- codex_usage_tracker/pricing_estimates.py +44 -0
- codex_usage_tracker/pricing_openai.py +253 -0
- codex_usage_tracker/projects.py +347 -0
- codex_usage_tracker/recommendations.py +270 -0
- codex_usage_tracker/reports.py +637 -0
- codex_usage_tracker/schema.py +71 -0
- codex_usage_tracker/server.py +400 -0
- codex_usage_tracker/store.py +666 -0
- codex_usage_tracker/support.py +147 -0
- codex_usage_tracker/threads.py +183 -0
- codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
- codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
- codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
- codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
- codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
- codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const short = (value, fallback = 'Unknown') => value || fallback;
|
|
3
|
+
|
|
4
|
+
function payloadRows(nextPayload) {
|
|
5
|
+
return Array.isArray(nextPayload) ? nextPayload : Array.isArray(nextPayload.rows) ? nextPayload.rows : [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function payloadLimit(nextPayload) {
|
|
9
|
+
if (!nextPayload || nextPayload.limit === null || nextPayload.limit === undefined) return null;
|
|
10
|
+
const parsed = Number(nextPayload.limit);
|
|
11
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function limitValue(limit) {
|
|
15
|
+
return limit === null || limit === undefined ? 'all' : String(limit);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function optionValueExists(select, value) {
|
|
19
|
+
if (!value) return false;
|
|
20
|
+
return Array.from(select.options || []).some(option => option.value === value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clamp(value, min, max) {
|
|
24
|
+
return Math.min(Math.max(value, min), max);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function usageCreditValue(row) {
|
|
28
|
+
return row.usage_credits === null || row.usage_credits === undefined ? null : Number(row.usage_credits || 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function usageCreditStatusText(row) {
|
|
32
|
+
if (usageCreditValue(row) === null) return 'No mapped Codex credit rate';
|
|
33
|
+
if (row.usage_credit_confidence === 'exact') return 'Official rate-card match';
|
|
34
|
+
if (row.usage_credit_confidence === 'estimated') return 'Inferred model mapping';
|
|
35
|
+
if (row.usage_credit_confidence === 'user_override') return 'User-provided credit rate';
|
|
36
|
+
return short(row.usage_credit_confidence, 'Configured rate');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sumUsageCredits(rows) {
|
|
40
|
+
return rows.reduce((sum, row) => {
|
|
41
|
+
const value = usageCreditValue(row);
|
|
42
|
+
return value === null ? sum : sum + value;
|
|
43
|
+
}, 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function creditCoverageRatio(rows) {
|
|
47
|
+
const totalTokens = rows.reduce((sum, row) => sum + Number(row.total_tokens || 0), 0);
|
|
48
|
+
const ratedTokens = rows.reduce((sum, row) => sum + (usageCreditValue(row) === null ? 0 : Number(row.total_tokens || 0)), 0);
|
|
49
|
+
return totalTokens ? ratedTokens / totalTokens : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isAutoReview(row) {
|
|
53
|
+
return row.model === 'codex-auto-review' || row.subagent_type === 'guardian';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isSubagent(row) {
|
|
57
|
+
return row.thread_source === 'subagent' || Boolean(row.subagent_type || row.parent_session_id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sourceLabel(row) {
|
|
61
|
+
if (isAutoReview(row)) return 'Auto-review';
|
|
62
|
+
if (row.subagent_type === 'thread_spawn') {
|
|
63
|
+
return row.agent_role ? `Subagent: ${row.agent_role}` : 'Subagent';
|
|
64
|
+
}
|
|
65
|
+
if (isSubagent(row)) return 'Subagent';
|
|
66
|
+
return 'User';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolvedParentThreadName(row) {
|
|
70
|
+
return row.resolved_parent_thread_name || row.parent_thread_name || '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolvedParentSessionUpdatedAt(row) {
|
|
74
|
+
return row.resolved_parent_session_updated_at || row.parent_session_updated_at || '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveThreadAttachment(row) {
|
|
78
|
+
if (row.thread_attachment_key && row.thread_attachment_label) {
|
|
79
|
+
return {
|
|
80
|
+
key: row.thread_attachment_key,
|
|
81
|
+
label: row.thread_attachment_label,
|
|
82
|
+
relation: row.thread_attachment_relation || 'session',
|
|
83
|
+
parentSessionId: row.thread_attachment_parent_session_id || row.parent_session_id || null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (row.thread_name) {
|
|
87
|
+
return { key: `thread:${row.thread_name}`, label: row.thread_name, relation: 'direct' };
|
|
88
|
+
}
|
|
89
|
+
const parentThreadName = resolvedParentThreadName(row);
|
|
90
|
+
if (row.parent_session_id && parentThreadName) {
|
|
91
|
+
return {
|
|
92
|
+
key: `thread:${parentThreadName}`,
|
|
93
|
+
label: parentThreadName,
|
|
94
|
+
relation: 'explicit parent thread',
|
|
95
|
+
parentSessionId: row.parent_session_id,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (row.parent_session_id) {
|
|
99
|
+
return {
|
|
100
|
+
key: `session:${row.parent_session_id}`,
|
|
101
|
+
label: `Parent ${row.parent_session_id}`,
|
|
102
|
+
relation: 'explicit parent',
|
|
103
|
+
parentSessionId: row.parent_session_id,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
key: `session:${row.session_id || 'unknown'}`,
|
|
108
|
+
label: row.session_id || 'Unknown thread',
|
|
109
|
+
relation: isSubagent(row) ? 'unmatched subagent' : 'session',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function chronological(a, b) {
|
|
114
|
+
const timeCompare = String(a.event_timestamp || '').localeCompare(String(b.event_timestamp || ''));
|
|
115
|
+
if (timeCompare !== 0) return timeCompare;
|
|
116
|
+
return Number(a.cumulative_total_tokens || 0) - Number(b.cumulative_total_tokens || 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function compactListSummary(values, fallback = 'Mixed') {
|
|
120
|
+
const unique = [...new Set(values.filter(Boolean))].sort();
|
|
121
|
+
if (!unique.length) return 'Unknown';
|
|
122
|
+
if (unique.length === 1) return unique[0];
|
|
123
|
+
return `${unique[0]} +${unique.length - 1} ${fallback.toLowerCase()}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function threadModelSummary(calls) {
|
|
127
|
+
const models = [...new Set(calls.map(row => row.model).filter(Boolean))].sort();
|
|
128
|
+
if (!models.length) return 'Unknown';
|
|
129
|
+
if (models.length === 1) return models[0];
|
|
130
|
+
const nonReviewModels = models.filter(model => model !== 'codex-auto-review');
|
|
131
|
+
const primary = nonReviewModels.length ? nonReviewModels[0] : models[0];
|
|
132
|
+
return `${primary} +${models.length - 1} models`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
window.CodexUsageDashboardData = Object.freeze({
|
|
136
|
+
payloadRows,
|
|
137
|
+
payloadLimit,
|
|
138
|
+
limitValue,
|
|
139
|
+
optionValueExists,
|
|
140
|
+
clamp,
|
|
141
|
+
usageCreditValue,
|
|
142
|
+
usageCreditStatusText,
|
|
143
|
+
sumUsageCredits,
|
|
144
|
+
creditCoverageRatio,
|
|
145
|
+
isAutoReview,
|
|
146
|
+
isSubagent,
|
|
147
|
+
sourceLabel,
|
|
148
|
+
resolvedParentThreadName,
|
|
149
|
+
resolvedParentSessionUpdatedAt,
|
|
150
|
+
resolveThreadAttachment,
|
|
151
|
+
chronological,
|
|
152
|
+
compactListSummary,
|
|
153
|
+
threadModelSummary,
|
|
154
|
+
});
|
|
155
|
+
})();
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const number = new Intl.NumberFormat();
|
|
3
|
+
const tableDateFormat = new Intl.DateTimeFormat([], {
|
|
4
|
+
month: 'short',
|
|
5
|
+
day: 'numeric',
|
|
6
|
+
year: 'numeric',
|
|
7
|
+
});
|
|
8
|
+
const tableTimeFormat = new Intl.DateTimeFormat([], {
|
|
9
|
+
hour: 'numeric',
|
|
10
|
+
minute: '2-digit',
|
|
11
|
+
second: '2-digit',
|
|
12
|
+
});
|
|
13
|
+
const detailDateTimeFormat = new Intl.DateTimeFormat([], {
|
|
14
|
+
month: 'short',
|
|
15
|
+
day: 'numeric',
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
hour: 'numeric',
|
|
18
|
+
minute: '2-digit',
|
|
19
|
+
second: '2-digit',
|
|
20
|
+
timeZoneName: 'short',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const money = (value, missingLabel = 'No price') => {
|
|
24
|
+
if (value === null || value === undefined) return missingLabel;
|
|
25
|
+
const amount = Number(value) || 0;
|
|
26
|
+
if (amount > 0 && amount < 0.01) return `$${amount.toFixed(4)}`;
|
|
27
|
+
return `$${amount.toFixed(2)}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const credits = (value, missingLabel = 'No rate') => {
|
|
31
|
+
if (value === null || value === undefined) return missingLabel;
|
|
32
|
+
const amount = Number(value) || 0;
|
|
33
|
+
if (amount > 0 && amount < 1) return amount.toFixed(2);
|
|
34
|
+
if (amount < 100) return amount.toFixed(1);
|
|
35
|
+
return number.format(Math.round(amount));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const pct = value => `${((Number(value) || 0) * 100).toFixed(1)}%`;
|
|
39
|
+
const short = (value, fallback = 'Unknown') => value || fallback;
|
|
40
|
+
const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({
|
|
41
|
+
'&': '&',
|
|
42
|
+
'<': '<',
|
|
43
|
+
'>': '>',
|
|
44
|
+
'"': '"',
|
|
45
|
+
"'": ''',
|
|
46
|
+
}[char]));
|
|
47
|
+
const truncate = (value, size = 54) => {
|
|
48
|
+
const text = short(value, '');
|
|
49
|
+
return text.length > size ? `${text.slice(0, size - 1)}...` : text;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function parsedTimestamp(value) {
|
|
53
|
+
if (!value) return null;
|
|
54
|
+
const date = new Date(value);
|
|
55
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatTimestamp(value, fallback = 'Unknown') {
|
|
59
|
+
const date = parsedTimestamp(value);
|
|
60
|
+
return date ? detailDateTimeFormat.format(date) : short(value, fallback);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatTimestampTitle(value) {
|
|
64
|
+
const formatted = formatTimestamp(value, '');
|
|
65
|
+
return [formatted, value].filter(Boolean).join(' - ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderTimeCell(value) {
|
|
69
|
+
const date = parsedTimestamp(value);
|
|
70
|
+
if (!date) return escapeHtml(truncate(value, 20));
|
|
71
|
+
return `
|
|
72
|
+
<span class="time-cell" title="${escapeHtml(formatTimestampTitle(value))}">
|
|
73
|
+
<span class="time-date">${escapeHtml(tableDateFormat.format(date))}</span>
|
|
74
|
+
<span class="time-clock">${escapeHtml(tableTimeFormat.format(date))}</span>
|
|
75
|
+
</span>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function defaultSortDirection(key) {
|
|
80
|
+
return {
|
|
81
|
+
cache: 'asc',
|
|
82
|
+
effort: 'asc',
|
|
83
|
+
model: 'asc',
|
|
84
|
+
thread: 'asc',
|
|
85
|
+
}[key] || 'desc';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function textValue(value) {
|
|
89
|
+
return short(value, '').toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compareValues(left, right) {
|
|
93
|
+
if (typeof left === 'number' || typeof right === 'number') {
|
|
94
|
+
return (Number(left) || 0) - (Number(right) || 0);
|
|
95
|
+
}
|
|
96
|
+
return String(left || '').localeCompare(String(right || ''));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sortLabel(key) {
|
|
100
|
+
return {
|
|
101
|
+
attention: 'Needs attention',
|
|
102
|
+
cache: 'Cache',
|
|
103
|
+
context: 'Context use',
|
|
104
|
+
cost: 'Cost',
|
|
105
|
+
effort: 'Effort',
|
|
106
|
+
model: 'Model',
|
|
107
|
+
signals: 'Signals',
|
|
108
|
+
thread: 'Thread',
|
|
109
|
+
time: 'Time',
|
|
110
|
+
total: 'Tokens',
|
|
111
|
+
usage: 'Codex credits',
|
|
112
|
+
}[key] || 'Sort';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
window.CodexUsageDashboardFormat = Object.freeze({
|
|
116
|
+
number,
|
|
117
|
+
money,
|
|
118
|
+
credits,
|
|
119
|
+
pct,
|
|
120
|
+
short,
|
|
121
|
+
escapeHtml,
|
|
122
|
+
truncate,
|
|
123
|
+
parsedTimestamp,
|
|
124
|
+
formatTimestamp,
|
|
125
|
+
formatTimestampTitle,
|
|
126
|
+
renderTimeCell,
|
|
127
|
+
defaultSortDirection,
|
|
128
|
+
textValue,
|
|
129
|
+
compareValues,
|
|
130
|
+
sortLabel,
|
|
131
|
+
});
|
|
132
|
+
})();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const ALLOWED_VIEWS = new Set(['insights', 'calls', 'threads']);
|
|
3
|
+
const ALLOWED_DIRECTIONS = new Set(['asc', 'desc']);
|
|
4
|
+
const STATE_KEYS = [
|
|
5
|
+
'view',
|
|
6
|
+
'q',
|
|
7
|
+
'model',
|
|
8
|
+
'effort',
|
|
9
|
+
'confidence',
|
|
10
|
+
'pricing',
|
|
11
|
+
'date',
|
|
12
|
+
'from',
|
|
13
|
+
'to',
|
|
14
|
+
'history',
|
|
15
|
+
'sort',
|
|
16
|
+
'direction',
|
|
17
|
+
'preset',
|
|
18
|
+
'page',
|
|
19
|
+
'record',
|
|
20
|
+
'thread',
|
|
21
|
+
'expand',
|
|
22
|
+
'threads',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function clean(value) {
|
|
26
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function read(params = new URLSearchParams(window.location.search)) {
|
|
30
|
+
const page = Number(params.get('page') || 1);
|
|
31
|
+
return {
|
|
32
|
+
view: ALLOWED_VIEWS.has(params.get('view')) ? params.get('view') : '',
|
|
33
|
+
search: clean(params.get('q')),
|
|
34
|
+
model: clean(params.get('model')),
|
|
35
|
+
effort: clean(params.get('effort')),
|
|
36
|
+
confidence: clean(params.get('confidence') || params.get('pricing')),
|
|
37
|
+
datePreset: clean(params.get('date')),
|
|
38
|
+
dateStart: clean(params.get('from')),
|
|
39
|
+
dateEnd: clean(params.get('to')),
|
|
40
|
+
historyScope: params.get('history') === 'all' ? 'all' : '',
|
|
41
|
+
sort: clean(params.get('sort')),
|
|
42
|
+
direction: ALLOWED_DIRECTIONS.has(params.get('direction')) ? params.get('direction') : '',
|
|
43
|
+
preset: clean(params.get('preset')),
|
|
44
|
+
page: Number.isFinite(page) && page > 0 ? Math.floor(page) : 1,
|
|
45
|
+
record: clean(params.get('record')),
|
|
46
|
+
thread: clean(params.get('thread')),
|
|
47
|
+
expand: clean(params.get('expand')),
|
|
48
|
+
expandedThreads: clean(params.get('threads')).split(',').filter(Boolean).slice(0, 30).map(decodePart),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function serialize(state) {
|
|
53
|
+
const params = new URLSearchParams(window.location.search);
|
|
54
|
+
STATE_KEYS.forEach(key => params.delete(key));
|
|
55
|
+
set(params, 'view', ALLOWED_VIEWS.has(state.view) && state.view !== 'insights' ? state.view : '');
|
|
56
|
+
set(params, 'q', state.search);
|
|
57
|
+
set(params, 'model', state.model);
|
|
58
|
+
set(params, 'effort', state.effort);
|
|
59
|
+
set(params, 'confidence', state.confidence);
|
|
60
|
+
set(params, 'date', state.datePreset && state.datePreset !== 'all' ? state.datePreset : '');
|
|
61
|
+
set(params, 'from', state.dateStart);
|
|
62
|
+
set(params, 'to', state.dateEnd);
|
|
63
|
+
set(params, 'history', state.historyScope === 'all' ? 'all' : '');
|
|
64
|
+
set(params, 'sort', state.sort && state.sort !== 'attention' ? state.sort : '');
|
|
65
|
+
set(params, 'direction', ALLOWED_DIRECTIONS.has(state.direction) ? state.direction : '');
|
|
66
|
+
set(params, 'preset', state.preset);
|
|
67
|
+
set(params, 'page', state.page && Number(state.page) > 1 ? String(Math.floor(Number(state.page))) : '');
|
|
68
|
+
set(params, 'record', state.record);
|
|
69
|
+
set(params, 'thread', state.thread);
|
|
70
|
+
set(params, 'expand', state.expand);
|
|
71
|
+
set(params, 'threads', Array.isArray(state.expandedThreads) ? state.expandedThreads.slice(0, 30).map(encodeURIComponent).join(',') : '');
|
|
72
|
+
return params;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function set(params, key, value) {
|
|
76
|
+
const text = clean(value);
|
|
77
|
+
if (text) params.set(key, text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function urlFor(state) {
|
|
81
|
+
const params = serialize(state);
|
|
82
|
+
const query = params.toString();
|
|
83
|
+
const base = window.location.href.split('#')[0].split('?')[0];
|
|
84
|
+
return `${base}${query ? `?${query}` : ''}${window.location.hash || ''}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function replace(state) {
|
|
88
|
+
if (!window.history || !window.history.replaceState) return;
|
|
89
|
+
const nextUrl = urlFor(state);
|
|
90
|
+
if (nextUrl !== window.location.href) {
|
|
91
|
+
window.history.replaceState(null, '', nextUrl);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function copyText(text) {
|
|
96
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
97
|
+
await navigator.clipboard.writeText(text);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const textarea = document.createElement('textarea');
|
|
101
|
+
textarea.value = text;
|
|
102
|
+
textarea.setAttribute('readonly', 'readonly');
|
|
103
|
+
textarea.style.position = 'fixed';
|
|
104
|
+
textarea.style.left = '-9999px';
|
|
105
|
+
document.body.appendChild(textarea);
|
|
106
|
+
textarea.select();
|
|
107
|
+
const copied = document.execCommand('copy');
|
|
108
|
+
textarea.remove();
|
|
109
|
+
return copied;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function downloadText(filename, text, type = 'text/plain;charset=utf-8') {
|
|
113
|
+
const blob = new Blob([text], { type });
|
|
114
|
+
const url = URL.createObjectURL(blob);
|
|
115
|
+
const anchor = document.createElement('a');
|
|
116
|
+
anchor.href = url;
|
|
117
|
+
anchor.download = filename;
|
|
118
|
+
document.body.appendChild(anchor);
|
|
119
|
+
anchor.click();
|
|
120
|
+
anchor.remove();
|
|
121
|
+
URL.revokeObjectURL(url);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toCsv(rows, columns) {
|
|
125
|
+
const header = columns.map(column => csvCell(column.label)).join(',');
|
|
126
|
+
const body = rows.map(row => columns.map(column => csvCell(resolveValue(row, column.field))).join(','));
|
|
127
|
+
return [header, ...body].join('\n') + '\n';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveValue(row, field) {
|
|
131
|
+
if (typeof field === 'function') return field(row);
|
|
132
|
+
return row && Object.prototype.hasOwnProperty.call(row, field) ? row[field] : '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function csvCell(value) {
|
|
136
|
+
if (value === null || value === undefined) return '';
|
|
137
|
+
const text = Array.isArray(value) ? value.join('; ') : String(value);
|
|
138
|
+
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function decodePart(value) {
|
|
142
|
+
try {
|
|
143
|
+
return decodeURIComponent(value);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
window.CodexUsageDashboardState = {
|
|
150
|
+
read,
|
|
151
|
+
replace,
|
|
152
|
+
urlFor,
|
|
153
|
+
copyText,
|
|
154
|
+
downloadText,
|
|
155
|
+
toCsv,
|
|
156
|
+
};
|
|
157
|
+
}());
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
7
|
+
<title>__TITLE__</title>
|
|
8
|
+
<link rel="stylesheet" href="__STYLESHEET_HREF__">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header>
|
|
12
|
+
<div class="header-main">
|
|
13
|
+
<div>
|
|
14
|
+
<p class="eyebrow">Local Codex analytics</p>
|
|
15
|
+
<h1>Usage Dashboard</h1>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="live-bar" aria-label="Dashboard refresh controls">
|
|
18
|
+
<button id="refreshDashboard" class="refresh-button" type="button">Refresh</button>
|
|
19
|
+
<label class="live-toggle"><input id="autoRefresh" type="checkbox" checked> Live</label>
|
|
20
|
+
<label class="load-control">Load
|
|
21
|
+
<select id="loadLimit">
|
|
22
|
+
<option value="5000">5,000 calls</option>
|
|
23
|
+
<option value="10000">10,000 calls</option>
|
|
24
|
+
<option value="20000">20,000 calls</option>
|
|
25
|
+
<option value="all">All calls</option>
|
|
26
|
+
</select>
|
|
27
|
+
</label>
|
|
28
|
+
<label class="history-control" title="Active sessions only is the default. All history scans archived session logs during live refresh.">History
|
|
29
|
+
<select id="historyScope">
|
|
30
|
+
<option value="active" selected>Active sessions only</option>
|
|
31
|
+
<option value="all">All history</option>
|
|
32
|
+
</select>
|
|
33
|
+
</label>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="header-meta">
|
|
37
|
+
<div class="meta-chips" aria-label="Dashboard status">
|
|
38
|
+
<span class="disclaimer-chip" title="Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.">Unofficial project</span>
|
|
39
|
+
<span id="liveStatus" class="live-status">Static</span>
|
|
40
|
+
<span id="pricingSource" class="source-line"></span>
|
|
41
|
+
<span id="allowanceSource" class="source-line"></span>
|
|
42
|
+
<span id="privacyMode" class="source-line"></span>
|
|
43
|
+
<span id="parserDiagnostics" class="source-line" hidden></span>
|
|
44
|
+
</div>
|
|
45
|
+
__GUIDE_LINK__
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
48
|
+
<main>
|
|
49
|
+
<div class="filters">
|
|
50
|
+
<label>Search<input id="search" type="search" placeholder="Thread, cwd, model"></label>
|
|
51
|
+
<label>Model<select id="model"><option value="">All models</option></select></label>
|
|
52
|
+
<label>Reasoning<select id="effort"><option value="">All efforts</option></select></label>
|
|
53
|
+
<label>Confidence<select id="pricingStatus"><option value="">All confidence</option><option value="official">Exact cost</option><option value="estimated">Estimated cost</option><option value="unpriced">Unpriced cost</option><option value="credit-exact">Exact credit rate</option><option value="credit-estimated">Estimated credit mapping</option><option value="credit-override">User credit override</option><option value="credit-missing">Missing credit rate</option></select></label>
|
|
54
|
+
<label>Time<select id="datePreset"><option value="all" selected>All time</option><option value="today">Today</option><option value="this-week">This week</option><option value="last-7-days">Last 7 days</option><option value="this-month">This month</option><option value="custom">Custom range</option></select><span id="dateRangeStatus" class="filter-status" aria-live="polite" hidden></span></label>
|
|
55
|
+
<label>Start<input id="dateStart" type="date"></label>
|
|
56
|
+
<label>End<input id="dateEnd" type="date"></label>
|
|
57
|
+
<label>Sort<select id="sort"><option value="attention" selected>Needs attention</option><option value="time">Newest calls</option><option value="thread">Thread name</option><option value="model">Model</option><option value="effort">Reasoning</option><option value="total">Most tokens</option><option value="cost">Highest estimated cost</option><option value="usage">Highest Codex credits</option><option value="cache">Lowest cache ratio</option><option value="signals">Most signals</option><option value="context">Highest context use</option></select></label>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="cards">
|
|
60
|
+
<div class="card"><span>Visible Calls</span><strong id="visibleCalls">0</strong></div>
|
|
61
|
+
<div class="card"><span>Total Tokens</span><strong id="totalTokens">0</strong></div>
|
|
62
|
+
<div class="card"><span>Cached Input</span><strong id="cachedTokens">0</strong></div>
|
|
63
|
+
<div class="card"><span>Uncached Input</span><strong id="uncachedTokens">0</strong></div>
|
|
64
|
+
<div class="card"><span>Reasoning Output</span><strong id="reasoningTokens">0</strong></div>
|
|
65
|
+
<div class="card"><span>Estimated Cost</span><strong id="estimatedCost">$0.00</strong></div>
|
|
66
|
+
<div class="card"><span>Codex Credits</span><strong id="usageCredits">0</strong></div>
|
|
67
|
+
<div class="card"><span>Usage Remaining</span><strong id="allowanceImpact">Set limits</strong></div>
|
|
68
|
+
</div>
|
|
69
|
+
<section id="insightsPanel" class="insights-panel" aria-labelledby="insightsTitle">
|
|
70
|
+
<div class="insights-layout">
|
|
71
|
+
<div>
|
|
72
|
+
<div class="section-heading">
|
|
73
|
+
<h2 id="insightsTitle">Needs Attention</h2>
|
|
74
|
+
<p id="insightsCaption">Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.</p>
|
|
75
|
+
</div>
|
|
76
|
+
<div id="insightCards" class="insight-cards" aria-live="polite"></div>
|
|
77
|
+
</div>
|
|
78
|
+
<aside class="preset-panel" aria-labelledby="presetsTitle">
|
|
79
|
+
<div class="preset-heading">
|
|
80
|
+
<div>
|
|
81
|
+
<h2 id="presetsTitle">Investigation Presets</h2>
|
|
82
|
+
<p>One-click starting points for common usage questions.</p>
|
|
83
|
+
</div>
|
|
84
|
+
<button id="clearPreset" class="preset-clear" type="button" hidden>Clear</button>
|
|
85
|
+
</div>
|
|
86
|
+
<div id="presetStatus" class="preset-status">No preset applied.</div>
|
|
87
|
+
<div id="presetList" class="preset-list"></div>
|
|
88
|
+
</aside>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
<div class="grid">
|
|
92
|
+
<section>
|
|
93
|
+
<h2 id="tableTitle">Model Calls</h2>
|
|
94
|
+
<div class="table-tools">
|
|
95
|
+
<div class="segmented" role="group" aria-label="Dashboard view">
|
|
96
|
+
<button id="insightsView" type="button" aria-pressed="true">Insights</button>
|
|
97
|
+
<button id="callsView" type="button" aria-pressed="false">Calls</button>
|
|
98
|
+
<button id="threadsView" type="button" aria-pressed="false">Threads</button>
|
|
99
|
+
</div>
|
|
100
|
+
<div id="tableCaption" class="table-caption">Showing individual model calls.</div>
|
|
101
|
+
<div class="table-actions" aria-label="Current view actions">
|
|
102
|
+
<button id="copyViewLink" class="toolbar-button" type="button">Copy link</button>
|
|
103
|
+
<button id="exportVisible" class="toolbar-button" type="button">Export CSV</button>
|
|
104
|
+
<span id="actionStatus" class="action-status" aria-live="polite"></span>
|
|
105
|
+
</div>
|
|
106
|
+
<div id="pager" class="pager" aria-label="Table pages">
|
|
107
|
+
<button id="prevPage" class="pager-button" type="button">Previous</button>
|
|
108
|
+
<span id="pageStatus">Page 1</span>
|
|
109
|
+
<button id="nextPage" class="pager-button" type="button">Next</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<table>
|
|
113
|
+
<thead>
|
|
114
|
+
<tr>
|
|
115
|
+
<th data-sort-header="time"><button class="sort-header" type="button" data-sort-key="time">Time <span class="sort-indicator" data-sort-indicator="time"></span></button></th>
|
|
116
|
+
<th data-sort-header="thread"><button class="sort-header" type="button" data-sort-key="thread">Thread <span class="sort-indicator" data-sort-indicator="thread"></span></button></th>
|
|
117
|
+
<th data-sort-header="model"><button class="sort-header" type="button" data-sort-key="model">Model <span class="sort-indicator" data-sort-indicator="model"></span></button></th>
|
|
118
|
+
<th data-sort-header="effort"><button class="sort-header" type="button" data-sort-key="effort">Effort <span class="sort-indicator" data-sort-indicator="effort"></span></button></th>
|
|
119
|
+
<th class="num" data-sort-header="total"><button class="sort-header" type="button" data-sort-key="total">Tokens <span class="sort-indicator" data-sort-indicator="total"></span></button></th>
|
|
120
|
+
<th class="num" data-sort-header="cost"><button class="sort-header" type="button" data-sort-key="cost">Cost <span class="sort-indicator" data-sort-indicator="cost"></span></button></th>
|
|
121
|
+
<th class="num" data-sort-header="cache"><button class="sort-header" type="button" data-sort-key="cache">Cache <span class="sort-indicator" data-sort-indicator="cache"></span></button></th>
|
|
122
|
+
<th class="num" data-sort-header="signals"><button class="sort-header" type="button" data-sort-key="signals">Signals <span class="sort-indicator" data-sort-indicator="signals"></span></button></th>
|
|
123
|
+
</tr>
|
|
124
|
+
</thead>
|
|
125
|
+
<tbody id="rows"></tbody>
|
|
126
|
+
</table>
|
|
127
|
+
</section>
|
|
128
|
+
<section class="detail-section">
|
|
129
|
+
<h2>Call Details</h2>
|
|
130
|
+
<div id="detail" class="detail">Hover or click a row to inspect aggregate usage fields.</div>
|
|
131
|
+
</section>
|
|
132
|
+
</div>
|
|
133
|
+
</main>
|
|
134
|
+
<button id="toTop" class="to-top" type="button" aria-label="Back to top">Top</button>
|
|
135
|
+
<script id="usage-data" type="application/json">__PAYLOAD__</script>
|
|
136
|
+
<script src="__FORMAT_SCRIPT_SRC__"></script>
|
|
137
|
+
<script src="__DATA_SCRIPT_SRC__"></script>
|
|
138
|
+
<script src="__STATE_SCRIPT_SRC__"></script>
|
|
139
|
+
<script src="__SCRIPT_SRC__"></script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|