mobygate 0.8.4 → 0.9.4
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.
- package/CHANGELOG.md +544 -0
- package/bin/mobygate.js +214 -0
- package/dashboard.css +1 -0
- package/index.html +1 -15
- package/inspector.html +200 -3
- package/lib/anthropic.js +6 -1
- package/lib/captures-index.js +524 -0
- package/lib/inference-runner.js +753 -0
- package/lib/openai-translation.js +146 -0
- package/lib/quiet.js +249 -0
- package/lib/request-capture.js +24 -0
- package/package.json +4 -1
- package/server.js +389 -1151
package/bin/mobygate.js
CHANGED
|
@@ -950,6 +950,12 @@ Usage:
|
|
|
950
950
|
changing the active model.
|
|
951
951
|
mobygate disconnect Remove moby entries from a client config.
|
|
952
952
|
Usage: mobygate disconnect <client-id>
|
|
953
|
+
mobygate captures Search the captures index. Subcommands:
|
|
954
|
+
query [text] Filter + list captures
|
|
955
|
+
show <id> Print a capture's summary
|
|
956
|
+
stats Aggregate stats over the index
|
|
957
|
+
rebuild Backfill index from disk captures
|
|
958
|
+
Run \`mobygate captures query --help\` for filters.
|
|
953
959
|
mobygate uninstall Remove installed services
|
|
954
960
|
mobygate version Print version + install mode + path
|
|
955
961
|
|
|
@@ -958,6 +964,213 @@ Repo: ${REPO_ROOT}
|
|
|
958
964
|
`);
|
|
959
965
|
}
|
|
960
966
|
|
|
967
|
+
// ---------- captures ----------
|
|
968
|
+
|
|
969
|
+
function capturesUsage() {
|
|
970
|
+
print(`mobygate captures — search the SQLite-indexed capture log
|
|
971
|
+
|
|
972
|
+
Usage:
|
|
973
|
+
mobygate captures query [text] [filters]
|
|
974
|
+
mobygate captures show <request_id>
|
|
975
|
+
mobygate captures stats
|
|
976
|
+
mobygate captures rebuild
|
|
977
|
+
|
|
978
|
+
Query filters (all optional, combinable):
|
|
979
|
+
--since <dur> 1h, 24h, 7d, 30d (default: all)
|
|
980
|
+
--model <substr> claude-opus-4-7, sonnet, etc. (LIKE match)
|
|
981
|
+
--session <key> exact session_key match
|
|
982
|
+
--status <s> ok | client_disconnect | error
|
|
983
|
+
--stop <reason> end_turn | tool_use | max_tokens | ...
|
|
984
|
+
--min-duration <ms> only requests slower than N ms
|
|
985
|
+
--max-duration <ms> only requests faster than N ms
|
|
986
|
+
--has-tools only requests that declared tools
|
|
987
|
+
--limit <n> default 20, max 1000
|
|
988
|
+
--json emit rows as JSON instead of a table
|
|
989
|
+
|
|
990
|
+
Text (positional) does a LIKE %text% over first/last user message + session_key.
|
|
991
|
+
|
|
992
|
+
Examples:
|
|
993
|
+
mobygate captures query --since 1h --status client_disconnect
|
|
994
|
+
mobygate captures query "webflow" --model opus --limit 5
|
|
995
|
+
mobygate captures query --has-tools --min-duration 5000
|
|
996
|
+
mobygate captures show 809ba4e41301425d8587ed64
|
|
997
|
+
`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function parseDuration(s) {
|
|
1001
|
+
if (!s) return null;
|
|
1002
|
+
const m = String(s).match(/^(\d+)\s*([smhd])?$/);
|
|
1003
|
+
if (!m) return null;
|
|
1004
|
+
const n = parseInt(m[1], 10);
|
|
1005
|
+
const unit = m[2] || 's';
|
|
1006
|
+
const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[unit];
|
|
1007
|
+
return n * mult;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function takeFlag(args, name) {
|
|
1011
|
+
const i = args.indexOf(name);
|
|
1012
|
+
if (i === -1) return null;
|
|
1013
|
+
const val = args[i + 1];
|
|
1014
|
+
args.splice(i, 2);
|
|
1015
|
+
return val;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function takeBool(args, name) {
|
|
1019
|
+
const i = args.indexOf(name);
|
|
1020
|
+
if (i === -1) return false;
|
|
1021
|
+
args.splice(i, 1);
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function truncate(s, n) {
|
|
1026
|
+
if (s == null) return '';
|
|
1027
|
+
s = String(s).replace(/\s+/g, ' ').trim();
|
|
1028
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function fmtDuration(ms) {
|
|
1032
|
+
if (ms == null) return '-';
|
|
1033
|
+
if (ms < 1000) return `${ms}ms`;
|
|
1034
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function fmtTokens(n) {
|
|
1038
|
+
if (n == null) return '-';
|
|
1039
|
+
if (n < 1000) return String(n);
|
|
1040
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
|
|
1041
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async function cmdCaptures() {
|
|
1045
|
+
const sub = process.argv[3];
|
|
1046
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
1047
|
+
capturesUsage();
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const idx = await import('../lib/captures-index.js');
|
|
1052
|
+
|
|
1053
|
+
if (sub === 'query') {
|
|
1054
|
+
const args = process.argv.slice(4);
|
|
1055
|
+
if (args.includes('--help') || args.includes('-h')) { capturesUsage(); return; }
|
|
1056
|
+
|
|
1057
|
+
const filters = {};
|
|
1058
|
+
const since = takeFlag(args, '--since');
|
|
1059
|
+
if (since) filters.sinceMs = parseDuration(since);
|
|
1060
|
+
const model = takeFlag(args, '--model');
|
|
1061
|
+
if (model) filters.model = model;
|
|
1062
|
+
const session = takeFlag(args, '--session');
|
|
1063
|
+
if (session) filters.sessionKey = session;
|
|
1064
|
+
const status = takeFlag(args, '--status');
|
|
1065
|
+
if (status) filters.status = status;
|
|
1066
|
+
const stop = takeFlag(args, '--stop');
|
|
1067
|
+
if (stop) filters.stopReason = stop;
|
|
1068
|
+
const minDur = takeFlag(args, '--min-duration');
|
|
1069
|
+
if (minDur) filters.minDurationMs = parseInt(minDur, 10);
|
|
1070
|
+
const maxDur = takeFlag(args, '--max-duration');
|
|
1071
|
+
if (maxDur) filters.maxDurationMs = parseInt(maxDur, 10);
|
|
1072
|
+
if (takeBool(args, '--has-tools')) filters.hasTools = true;
|
|
1073
|
+
const limit = takeFlag(args, '--limit');
|
|
1074
|
+
if (limit) filters.limit = parseInt(limit, 10);
|
|
1075
|
+
const asJson = takeBool(args, '--json');
|
|
1076
|
+
// Remaining positional args are the search text.
|
|
1077
|
+
const text = args.filter((a) => !a.startsWith('--')).join(' ');
|
|
1078
|
+
if (text) filters.text = text;
|
|
1079
|
+
|
|
1080
|
+
const { rows, error } = await idx.queryCaptures(filters);
|
|
1081
|
+
if (error) die(`captures query failed: ${error}`);
|
|
1082
|
+
|
|
1083
|
+
if (asJson) { print(JSON.stringify(rows, null, 2)); return; }
|
|
1084
|
+
|
|
1085
|
+
if (!rows.length) {
|
|
1086
|
+
print(c.dim('(no captures match)'));
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Header
|
|
1091
|
+
print(
|
|
1092
|
+
c.bold('TIME '.padEnd(17)) +
|
|
1093
|
+
c.bold('REQUEST_ID '.padEnd(16)) +
|
|
1094
|
+
c.bold('STATUS '.padEnd(15)) +
|
|
1095
|
+
c.bold('STOP '.padEnd(12)) +
|
|
1096
|
+
c.bold('DUR '.padEnd(8)) +
|
|
1097
|
+
c.bold('IN '.padEnd(6)) +
|
|
1098
|
+
c.bold('OUT '.padEnd(6)) +
|
|
1099
|
+
c.bold('CACHE '.padEnd(7)) +
|
|
1100
|
+
c.bold('PREVIEW')
|
|
1101
|
+
);
|
|
1102
|
+
print(c.dim('─'.repeat(110)));
|
|
1103
|
+
for (const r of rows) {
|
|
1104
|
+
const ts = (r.ts || '').slice(5, 19).replace('T', ' ');
|
|
1105
|
+
const idShort = (r.request_id || '').slice(0, 14);
|
|
1106
|
+
const stColor = r.status === 'ok' ? c.green : r.status === 'client_disconnect' ? c.yell : r.status ? c.red : c.dim;
|
|
1107
|
+
const status = stColor((r.status || '-').padEnd(13));
|
|
1108
|
+
const stop = (r.stop_reason || '-').padEnd(10);
|
|
1109
|
+
const dur = fmtDuration(r.duration_ms).padEnd(6);
|
|
1110
|
+
const inT = fmtTokens(r.total_input_tokens).padEnd(4);
|
|
1111
|
+
const outT = fmtTokens(r.output_tokens).padEnd(4);
|
|
1112
|
+
const hit = r.cache_hit_pct != null ? `${r.cache_hit_pct.toFixed(0)}%`.padEnd(5) : '- ';
|
|
1113
|
+
const preview = truncate(r.first_user_text || r.last_user_text || '', 50);
|
|
1114
|
+
print(`${ts.padEnd(17)}${idShort.padEnd(16)}${status} ${stop} ${dur} ${inT} ${outT} ${hit} ${c.dim(preview)}`);
|
|
1115
|
+
}
|
|
1116
|
+
print(c.dim(`\n${rows.length} row${rows.length === 1 ? '' : 's'}`));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (sub === 'show') {
|
|
1121
|
+
const id = process.argv[4];
|
|
1122
|
+
if (!id) die('usage: mobygate captures show <request_id>');
|
|
1123
|
+
const row = await idx.getCapture(id);
|
|
1124
|
+
if (!row) die(`no capture found for request_id=${id}`);
|
|
1125
|
+
if (row.summary_path && existsSync(row.summary_path)) {
|
|
1126
|
+
print(readFileSync(row.summary_path, 'utf8'));
|
|
1127
|
+
} else {
|
|
1128
|
+
print(JSON.stringify(row, null, 2));
|
|
1129
|
+
}
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (sub === 'stats') {
|
|
1134
|
+
const s = await idx.captureStats();
|
|
1135
|
+
if (s.error) die(`stats failed: ${s.error}`);
|
|
1136
|
+
print(c.bold(`Captures index: ${s.total} rows`));
|
|
1137
|
+
print('');
|
|
1138
|
+
print(c.bold('By status:'));
|
|
1139
|
+
for (const r of s.byStatus) print(` ${(r.status || '(none)').padEnd(20)} ${r.n}`);
|
|
1140
|
+
print('');
|
|
1141
|
+
print(c.bold('Top models:'));
|
|
1142
|
+
for (const r of s.byModel) print(` ${(r.m || '(none)').padEnd(28)} ${r.n}`);
|
|
1143
|
+
print('');
|
|
1144
|
+
print(c.bold('By stop_reason:'));
|
|
1145
|
+
for (const r of s.byStop) print(` ${(r.stop_reason || '(none)').padEnd(20)} ${r.n}`);
|
|
1146
|
+
print('');
|
|
1147
|
+
print(c.bold('Token totals:'));
|
|
1148
|
+
print(` input (uncached): ${fmtTokens(s.tokens.in_t)}`);
|
|
1149
|
+
print(` cache_read: ${fmtTokens(s.tokens.cr_t)}`);
|
|
1150
|
+
print(` cache_create: ${fmtTokens(s.tokens.cc_t)}`);
|
|
1151
|
+
print(` output: ${fmtTokens(s.tokens.out_t)}`);
|
|
1152
|
+
print(` avg cache hit: ${s.tokens.avg_hit ? s.tokens.avg_hit.toFixed(1) + '%' : '-'}`);
|
|
1153
|
+
print(` avg duration: ${fmtDuration(Math.round(s.tokens.avg_ms || 0))}`);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (sub === 'rebuild') {
|
|
1158
|
+
info('Backfilling captures index from ~/.mobygate/captures/ — this scans every .json file.');
|
|
1159
|
+
const start = Date.now();
|
|
1160
|
+
const result = await idx.rebuildFromFilesystem((p) => {
|
|
1161
|
+
process.stdout.write(`\r indexed ${p.indexed}/${p.scanned} ...`);
|
|
1162
|
+
});
|
|
1163
|
+
process.stdout.write('\r');
|
|
1164
|
+
if (result.error) die(`rebuild failed: ${result.error}`);
|
|
1165
|
+
const ms = Date.now() - start;
|
|
1166
|
+
ok(`Indexed ${result.indexed} captures (${result.errors} errors, ${ms}ms).`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
capturesUsage();
|
|
1171
|
+
process.exit(1);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
961
1174
|
// ---------- dispatch ----------
|
|
962
1175
|
|
|
963
1176
|
const cmd = process.argv[2];
|
|
@@ -965,6 +1178,7 @@ const COMMANDS = {
|
|
|
965
1178
|
init: cmdInit,
|
|
966
1179
|
connect: cmdConnect,
|
|
967
1180
|
disconnect: cmdDisconnect,
|
|
1181
|
+
captures: cmdCaptures,
|
|
968
1182
|
update: cmdUpdate,
|
|
969
1183
|
upgrade: cmdUpdate,
|
|
970
1184
|
doctor: cmdDoctor,
|
package/dashboard.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.z-50{z-index:50}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.-ml-3{margin-left:-.75rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-auto{margin-top:auto}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-\[110px\]{height:110px}.h-\[22px\]{height:22px}.h-full{height:100%}.max-h-\[180px\]{max-height:180px}.max-h-\[240px\]{max-height:240px}.max-h-\[40vh\]{max-height:40vh}.max-h-\[52vh\]{max-height:52vh}.max-h-\[70vh\]{max-height:70vh}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-\[100px\]{width:100px}.w-\[110px\]{width:110px}.w-\[160px\]{width:160px}.w-\[180px\]{width:180px}.w-\[20\%\]{width:20%}.w-\[60px\]{width:60px}.w-\[6px\]{width:6px}.w-\[70px\]{width:70px}.w-\[72px\]{width:72px}.w-\[80px\]{width:80px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0}.max-w-3xl{max-width:48rem}.max-w-\[1440px\]{max-width:1440px}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-0{flex-basis:0px}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-\[22px\]{gap:22px}.gap-\[3px\]{gap:3px}.gap-\[5px\]{gap:5px}.overflow-auto{overflow:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-\[\#1A1A15\]{--tw-border-opacity:1;border-color:rgb(26 26 21/var(--tw-border-opacity,1))}.border-\[\#2A2A1F\]{--tw-border-opacity:1;border-color:rgb(42 42 31/var(--tw-border-opacity,1))}.border-\[\#B7E56D\]{--tw-border-opacity:1;border-color:rgb(183 229 109/var(--tw-border-opacity,1))}.border-l-\[\#4EA4C4\]{--tw-border-opacity:1;border-left-color:rgb(78 164 196/var(--tw-border-opacity,1))}.border-l-\[\#B7E56D\]{--tw-border-opacity:1;border-left-color:rgb(183 229 109/var(--tw-border-opacity,1))}.border-l-\[\#E89B2E\]{--tw-border-opacity:1;border-left-color:rgb(232 155 46/var(--tw-border-opacity,1))}.bg-\[\#0B0B09\]{--tw-bg-opacity:1;background-color:rgb(11 11 9/var(--tw-bg-opacity,1))}.bg-\[\#121210\]{--tw-bg-opacity:1;background-color:rgb(18 18 16/var(--tw-bg-opacity,1))}.bg-\[\#1A1A15\]{--tw-bg-opacity:1;background-color:rgb(26 26 21/var(--tw-bg-opacity,1))}.bg-\[\#2A2A1F\]{--tw-bg-opacity:1;background-color:rgb(42 42 31/var(--tw-bg-opacity,1))}.bg-\[\#4EA4C4\]{--tw-bg-opacity:1;background-color:rgb(78 164 196/var(--tw-bg-opacity,1))}.bg-\[\#5A5F54\]{--tw-bg-opacity:1;background-color:rgb(90 95 84/var(--tw-bg-opacity,1))}.bg-\[\#B7E56D1A\]{background-color:#b7e56d1a}.bg-\[\#B7E56D\]{--tw-bg-opacity:1;background-color:rgb(183 229 109/var(--tw-bg-opacity,1))}.bg-\[\#E89B2E\]{--tw-bg-opacity:1;background-color:rgb(232 155 46/var(--tw-bg-opacity,1))}.bg-black\/70{background-color:rgba(0,0,0,.7)}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-\[14px\]{padding-top:14px;padding-bottom:14px}.py-\[18px\]{padding-top:18px;padding-bottom:18px}.py-\[22px\]{padding-top:22px;padding-bottom:22px}.py-\[3px\]{padding-top:3px;padding-bottom:3px}.pb-2{padding-bottom:.5rem}.pb-7{padding-bottom:1.75rem}.pr-1{padding-right:.25rem}.pt-2{padding-top:.5rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-right{text-align:right}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[64px\]{font-size:64px}.text-\[9px\]{font-size:9px}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.leading-3{line-height:.75rem}.leading-4{line-height:1rem}.leading-8{line-height:2rem}.leading-\[14px\]{line-height:14px}.leading-\[15px\]{line-height:15px}.leading-\[16px\]{line-height:16px}.leading-\[18px\]{line-height:18px}.leading-\[56px\]{line-height:56px}.tracking-\[0\.04em\]{letter-spacing:.04em}.tracking-\[0\.06em\]{letter-spacing:.06em}.tracking-\[0\.08em\]{letter-spacing:.08em}.tracking-\[0\.12em\]{letter-spacing:.12em}.tracking-\[0\.14em\]{letter-spacing:.14em}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.22em\]{letter-spacing:.22em}.tracking-widest{letter-spacing:.1em}.text-\[\#0B0B09\]{--tw-text-opacity:1;color:rgb(11 11 9/var(--tw-text-opacity,1))}.text-\[\#4EA4C4\]{--tw-text-opacity:1;color:rgb(78 164 196/var(--tw-text-opacity,1))}.text-\[\#5A5F54\]{--tw-text-opacity:1;color:rgb(90 95 84/var(--tw-text-opacity,1))}.text-\[\#8A9A6A\]{--tw-text-opacity:1;color:rgb(138 154 106/var(--tw-text-opacity,1))}.text-\[\#B7E56D\]{--tw-text-opacity:1;color:rgb(183 229 109/var(--tw-text-opacity,1))}.text-\[\#C9D9A8\]{--tw-text-opacity:1;color:rgb(201 217 168/var(--tw-text-opacity,1))}.text-\[\#E89B2E\]{--tw-text-opacity:1;color:rgb(232 155 46/var(--tw-text-opacity,1))}.text-\[\#F3EFE4\]{--tw-text-opacity:1;color:rgb(243 239 228/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-\[\#4EA4C4\]{accent-color:#4ea4c4}.opacity-50{opacity:.5}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:border-\[\#5A5F54\]:hover{--tw-border-opacity:1;border-color:rgb(90 95 84/var(--tw-border-opacity,1))}.hover\:bg-\[\#1A1F12\]:hover{--tw-bg-opacity:1;background-color:rgb(26 31 18/var(--tw-bg-opacity,1))}.hover\:text-\[\#C9D9A8\]:hover{--tw-text-opacity:1;color:rgb(201 217 168/var(--tw-text-opacity,1))}.hover\:brightness-110:hover{--tw-brightness:brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}
|
package/index.html
CHANGED
|
@@ -7,21 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=VT323&display=swap" rel="stylesheet">
|
|
10
|
-
<
|
|
11
|
-
<script>
|
|
12
|
-
/* Design tokens transcribed from the Paper artboard C1-0.
|
|
13
|
-
Using arbitrary Tailwind values for exact fidelity with the design. */
|
|
14
|
-
tailwind.config = {
|
|
15
|
-
theme: {
|
|
16
|
-
extend: {
|
|
17
|
-
fontFamily: {
|
|
18
|
-
display: ['VT323', 'system-ui', 'monospace'],
|
|
19
|
-
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'],
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
</script>
|
|
10
|
+
<link rel="stylesheet" href="/dashboard.css">
|
|
25
11
|
<style>
|
|
26
12
|
html, body { background: #0B0B09; color: #F3EFE4; font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace; }
|
|
27
13
|
.whale-ascii {
|
package/inspector.html
CHANGED
|
@@ -121,12 +121,61 @@
|
|
|
121
121
|
::-webkit-scrollbar-track { background: var(--bg); }
|
|
122
122
|
::-webkit-scrollbar-thumb { background: var(--border); }
|
|
123
123
|
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
|
124
|
+
|
|
125
|
+
/* Tabs */
|
|
126
|
+
.tabs { display: flex; gap: 0; }
|
|
127
|
+
.tab {
|
|
128
|
+
padding: 6px 14px; border: 1px solid var(--border); background: var(--bg);
|
|
129
|
+
cursor: pointer; color: var(--dim); font-size: 12px; border-right: none;
|
|
130
|
+
}
|
|
131
|
+
.tab:last-child { border-right: 1px solid var(--border); }
|
|
132
|
+
.tab.active { background: var(--bg-3); color: var(--accent); border-color: var(--accent); }
|
|
133
|
+
.tab:hover { color: var(--fg); }
|
|
134
|
+
|
|
135
|
+
.view { display: none; }
|
|
136
|
+
.view.active { display: grid; grid-template-columns: 460px 1fr; height: calc(100vh - 53px); }
|
|
137
|
+
.view-full { display: none; }
|
|
138
|
+
.view-full.active { display: block; height: calc(100vh - 53px); overflow-y: auto; padding: 20px 28px; }
|
|
139
|
+
|
|
140
|
+
/* Sessions view */
|
|
141
|
+
.totals-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; }
|
|
142
|
+
.totals-grid .stat { padding: 12px 14px; border: 1px solid var(--border); background: var(--bg-2); }
|
|
143
|
+
.bucket-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 24px; }
|
|
144
|
+
.bucket-cell { padding: 10px 12px; border: 1px solid var(--border); background: var(--bg-2); }
|
|
145
|
+
.bucket-cell .lbl { color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
146
|
+
.bucket-cell .pct { font-family: 'VT323', monospace; font-size: 22px; line-height: 1; margin-top: 2px; }
|
|
147
|
+
.bucket-cell .pct.bleed { color: var(--crit); }
|
|
148
|
+
.bucket-cell .pct.warm { color: var(--hit); }
|
|
149
|
+
.bucket-cell .sub { color: var(--dim); font-size: 11px; margin-top: 2px; }
|
|
150
|
+
|
|
151
|
+
.sessions-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
152
|
+
.sessions-table th {
|
|
153
|
+
text-align: left; padding: 8px 10px; color: var(--dim); font-weight: 500;
|
|
154
|
+
border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg);
|
|
155
|
+
text-transform: uppercase; font-size: 10px; letter-spacing: 0.05em;
|
|
156
|
+
}
|
|
157
|
+
.sessions-table td {
|
|
158
|
+
padding: 8px 10px; border-bottom: 1px solid var(--border); color: var(--fg);
|
|
159
|
+
}
|
|
160
|
+
.sessions-table tr:hover td { background: var(--bg-2); }
|
|
161
|
+
.sessions-table .num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
162
|
+
.sessions-table .cost-high { color: var(--crit); font-weight: 500; }
|
|
163
|
+
.sessions-table .cost-med { color: var(--warn); }
|
|
164
|
+
.sessions-table .bucket-singleton { color: var(--crit); }
|
|
165
|
+
.sessions-table .bucket-short { color: var(--warn); }
|
|
166
|
+
.sessions-table .bucket-medium { color: var(--dim); }
|
|
167
|
+
.sessions-table .bucket-warm { color: var(--hit); }
|
|
168
|
+
.sessions-table .preview { color: var(--dim); max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
169
|
+
.sessions-table .sk { font-family: 'JetBrains Mono', monospace; color: var(--dim); }
|
|
124
170
|
</style>
|
|
125
171
|
</head>
|
|
126
172
|
<body>
|
|
127
173
|
<header>
|
|
128
174
|
<div class="title display">mobygate :: inspector</div>
|
|
129
|
-
<div class="
|
|
175
|
+
<div class="tabs">
|
|
176
|
+
<div class="tab active" data-tab="captures">Captures</div>
|
|
177
|
+
<div class="tab" data-tab="sessions">Sessions</div>
|
|
178
|
+
</div>
|
|
130
179
|
<div class="spacer"></div>
|
|
131
180
|
<button id="toggle" class="toggle">
|
|
132
181
|
<span class="dot"></span>
|
|
@@ -135,7 +184,8 @@
|
|
|
135
184
|
<a href="/">← dashboard</a>
|
|
136
185
|
</header>
|
|
137
186
|
|
|
138
|
-
|
|
187
|
+
<!-- Captures tab (existing two-pane layout) -->
|
|
188
|
+
<div class="view active" id="view-captures">
|
|
139
189
|
<div class="list-pane" id="list-pane">
|
|
140
190
|
<div class="list-stats" id="list-stats">loading…</div>
|
|
141
191
|
<div id="list"></div>
|
|
@@ -145,6 +195,13 @@
|
|
|
145
195
|
</div>
|
|
146
196
|
</div>
|
|
147
197
|
|
|
198
|
+
<!-- Sessions tab (new in v0.8.5) -->
|
|
199
|
+
<div class="view-full" id="view-sessions">
|
|
200
|
+
<div id="sessions-content">
|
|
201
|
+
<div style="padding:40px;color:var(--dim);text-align:center">loading session costs…</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
148
205
|
<script>
|
|
149
206
|
let selectedFilename = null;
|
|
150
207
|
let captureCache = []; // last fetch result, used to compute deltas
|
|
@@ -412,11 +469,151 @@
|
|
|
412
469
|
return head + `<div class="placeholder">… ${msgs.length - 15} messages collapsed …</div>` + tail;
|
|
413
470
|
}
|
|
414
471
|
|
|
472
|
+
// ──────────── tab switching ────────────
|
|
473
|
+
|
|
474
|
+
let activeTab = 'captures';
|
|
475
|
+
let sessionsLoaded = false;
|
|
476
|
+
|
|
477
|
+
document.querySelectorAll('.tab').forEach(t => {
|
|
478
|
+
t.addEventListener('click', () => {
|
|
479
|
+
const tab = t.dataset.tab;
|
|
480
|
+
document.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x.dataset.tab === tab));
|
|
481
|
+
document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === `view-${tab}`));
|
|
482
|
+
document.querySelectorAll('.view-full').forEach(v => v.classList.toggle('active', v.id === `view-${tab}`));
|
|
483
|
+
activeTab = tab;
|
|
484
|
+
if (tab === 'sessions') refreshSessions();
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ──────────── sessions view ────────────
|
|
489
|
+
|
|
490
|
+
async function refreshSessions() {
|
|
491
|
+
try {
|
|
492
|
+
const r = await fetch('/dashboard/session-costs');
|
|
493
|
+
const data = await r.json();
|
|
494
|
+
renderSessions(data);
|
|
495
|
+
sessionsLoaded = true;
|
|
496
|
+
} catch (e) {
|
|
497
|
+
document.getElementById('sessions-content').innerHTML =
|
|
498
|
+
`<div class="placeholder">failed: ${esc(e.message)}</div>`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function renderSessions(data) {
|
|
503
|
+
const sessions = data.sessions || [];
|
|
504
|
+
const buckets = data.buckets || {};
|
|
505
|
+
|
|
506
|
+
// Header stats
|
|
507
|
+
const totalsHtml = `
|
|
508
|
+
<div class="totals-grid">
|
|
509
|
+
<div class="stat">
|
|
510
|
+
<div class="label">total cost</div>
|
|
511
|
+
<div class="val accent">$${(data.total_cost_usd || 0).toFixed(2)}</div>
|
|
512
|
+
<div class="sub">across all tracked turns</div>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="stat">
|
|
515
|
+
<div class="label">sessions</div>
|
|
516
|
+
<div class="val">${data.session_count || 0}</div>
|
|
517
|
+
<div class="sub">distinct session keys</div>
|
|
518
|
+
</div>
|
|
519
|
+
<div class="stat">
|
|
520
|
+
<div class="label">turns</div>
|
|
521
|
+
<div class="val">${fmt(data.total_turns || 0)}</div>
|
|
522
|
+
<div class="sub">non-tool-use end_turns only</div>
|
|
523
|
+
</div>
|
|
524
|
+
<div class="stat">
|
|
525
|
+
<div class="label">avg / turn</div>
|
|
526
|
+
<div class="val ${(data.total_cost_usd / Math.max(data.total_turns,1)) > 0.15 ? 'warn' : ''}">$${((data.total_cost_usd || 0) / Math.max(data.total_turns, 1)).toFixed(3)}</div>
|
|
527
|
+
<div class="sub">lower = better cache discipline</div>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
`;
|
|
531
|
+
|
|
532
|
+
// Bucket breakdown
|
|
533
|
+
const order = ['warm', 'medium', 'short', 'singleton'];
|
|
534
|
+
const labels = {
|
|
535
|
+
warm: { name: '11+ turns (warm)', desc: 'cache amortized', cls: 'warm' },
|
|
536
|
+
medium: { name: '4-10 turns (medium)', desc: 'partial amortize', cls: '' },
|
|
537
|
+
short: { name: '2-3 turns (short)', desc: 'limited amortize', cls: '' },
|
|
538
|
+
singleton: { name: '1 turn (singleton)', desc: 'cache_creation tax — bleed', cls: 'bleed' },
|
|
539
|
+
};
|
|
540
|
+
const bucketsHtml = order.map(k => {
|
|
541
|
+
const b = buckets[k] || { sessions: 0, cost: 0, pct_of_total: 0 };
|
|
542
|
+
const lbl = labels[k];
|
|
543
|
+
return `
|
|
544
|
+
<div class="bucket-cell">
|
|
545
|
+
<div class="lbl">${lbl.name}</div>
|
|
546
|
+
<div class="pct ${lbl.cls}">${b.pct_of_total}%</div>
|
|
547
|
+
<div class="sub">${b.sessions} sessions · $${b.cost.toFixed(2)} · ${lbl.desc}</div>
|
|
548
|
+
</div>
|
|
549
|
+
`;
|
|
550
|
+
}).join('');
|
|
551
|
+
|
|
552
|
+
// Session table
|
|
553
|
+
const rowsHtml = sessions.map(s => {
|
|
554
|
+
const costClass = s.cost_usd > 1.0 ? 'cost-high' : s.cost_usd > 0.3 ? 'cost-med' : '';
|
|
555
|
+
const lastSeen = s.last_seen ? s.last_seen.replace(/_/, ' ').replace(/-/g, ':').slice(0, 16) : '?';
|
|
556
|
+
return `
|
|
557
|
+
<tr>
|
|
558
|
+
<td><span class="sk">${esc(s.session_key.slice(0, 22))}</span></td>
|
|
559
|
+
<td class="bucket-${s.bucket}">${s.bucket}</td>
|
|
560
|
+
<td class="num">${s.turns}</td>
|
|
561
|
+
<td class="num ${costClass}">$${s.cost_usd.toFixed(4)}</td>
|
|
562
|
+
<td class="num">$${s.per_turn_usd.toFixed(4)}</td>
|
|
563
|
+
<td>${esc(s.model || '?')}</td>
|
|
564
|
+
<td>${esc((s.path || '').replace('/v1/', ''))}</td>
|
|
565
|
+
<td class="preview">${esc(s.first_user || '(no first message captured)')}</td>
|
|
566
|
+
</tr>
|
|
567
|
+
`;
|
|
568
|
+
}).join('');
|
|
569
|
+
|
|
570
|
+
document.getElementById('sessions-content').innerHTML = `
|
|
571
|
+
<h2 style="font-family:'VT323',monospace;color:var(--accent);font-size:24px;margin:0 0 4px">Sessions cost breakdown</h2>
|
|
572
|
+
<div style="color:var(--dim);font-size:11px;margin-bottom:20px">
|
|
573
|
+
Aggregated from <code>[model-billed]</code> log lines. Updated each request. Generated ${new Date(data.generatedAt).toLocaleTimeString()}.
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
${totalsHtml}
|
|
577
|
+
|
|
578
|
+
<h3 style="color:var(--accent);font-size:12px;text-transform:uppercase;letter-spacing:0.05em;margin:16px 0 10px">Cost by session warmth</h3>
|
|
579
|
+
<div class="bucket-grid">${bucketsHtml}</div>
|
|
580
|
+
|
|
581
|
+
<h3 style="color:var(--accent);font-size:12px;text-transform:uppercase;letter-spacing:0.05em;margin:24px 0 10px">All sessions (by cost, descending)</h3>
|
|
582
|
+
<table class="sessions-table">
|
|
583
|
+
<thead>
|
|
584
|
+
<tr>
|
|
585
|
+
<th>session_key</th>
|
|
586
|
+
<th>bucket</th>
|
|
587
|
+
<th class="num">turns</th>
|
|
588
|
+
<th class="num">cost</th>
|
|
589
|
+
<th class="num">$/turn</th>
|
|
590
|
+
<th>model</th>
|
|
591
|
+
<th>shape</th>
|
|
592
|
+
<th>first user message</th>
|
|
593
|
+
</tr>
|
|
594
|
+
</thead>
|
|
595
|
+
<tbody>${rowsHtml}</tbody>
|
|
596
|
+
</table>
|
|
597
|
+
|
|
598
|
+
<div style="color:var(--dim);font-size:11px;margin-top:24px;line-height:1.5">
|
|
599
|
+
<b>How to read this:</b><br>
|
|
600
|
+
• <span style="color:var(--hit)">Warm</span> sessions = cache working, low $/turn — these are the wins.<br>
|
|
601
|
+
• <span style="color:var(--crit)">Singleton</span> sessions = sessions that fired once and never got cache amortization. High % here means money is bleeding to cache_creation tax on idle channels.<br>
|
|
602
|
+
• If a singleton is > $0.50, it's typically a one-shot from a rarely-visited Discord channel.<br>
|
|
603
|
+
• <b>Mitigation:</b> consolidate channels, or set up an OpenClaw cron to ping high-value channels every 4 min to keep their wire cache warm.
|
|
604
|
+
</div>
|
|
605
|
+
`;
|
|
606
|
+
}
|
|
607
|
+
|
|
415
608
|
// ──────────── kickoff ────────────
|
|
416
609
|
|
|
417
610
|
refreshToggleState();
|
|
418
611
|
refreshList();
|
|
419
|
-
setInterval(() => {
|
|
612
|
+
setInterval(() => {
|
|
613
|
+
if (activeTab === 'captures') { refreshList(); }
|
|
614
|
+
if (activeTab === 'sessions' && sessionsLoaded) { refreshSessions(); }
|
|
615
|
+
refreshToggleState();
|
|
616
|
+
}, 5000);
|
|
420
617
|
</script>
|
|
421
618
|
</body>
|
|
422
619
|
</html>
|
package/lib/anthropic.js
CHANGED
|
@@ -46,7 +46,12 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
46
46
|
*/
|
|
47
47
|
export function extractSdkUsage(message) {
|
|
48
48
|
if (!message) return { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, modelUsage: null };
|
|
49
|
-
|
|
49
|
+
// Order: explicit `.usage` (SDK result message), inner Anthropic
|
|
50
|
+
// Message `.message.usage` (assistant turn — present on every turn the
|
|
51
|
+
// SDK emits, so we can capture usage even when the loop is aborted on
|
|
52
|
+
// tool_use before the SDK delivers its final `result`), then flat fields
|
|
53
|
+
// on the message itself as a last-resort fallback.
|
|
54
|
+
const u = message.usage || message.message?.usage || message;
|
|
50
55
|
return {
|
|
51
56
|
input_tokens: u.input_tokens || 0,
|
|
52
57
|
output_tokens: u.output_tokens || 0,
|