aiopt 0.3.2 → 0.3.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/dist/cli.js +144 -113
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -656,7 +656,7 @@ var require_package = __commonJS({
|
|
|
656
656
|
"package.json"(exports2, module2) {
|
|
657
657
|
module2.exports = {
|
|
658
658
|
name: "aiopt",
|
|
659
|
-
version: "0.3.
|
|
659
|
+
version: "0.3.4",
|
|
660
660
|
description: "Pre-deploy LLM cost accident guardrail (serverless local CLI)",
|
|
661
661
|
bin: {
|
|
662
662
|
aiopt: "dist/cli.js"
|
|
@@ -1667,10 +1667,6 @@ var init_guard = __esm({
|
|
|
1667
1667
|
});
|
|
1668
1668
|
|
|
1669
1669
|
// src/collect.ts
|
|
1670
|
-
var collect_exports = {};
|
|
1671
|
-
__export(collect_exports, {
|
|
1672
|
-
collectToUsageJsonl: () => collectToUsageJsonl
|
|
1673
|
-
});
|
|
1674
1670
|
function exists(p) {
|
|
1675
1671
|
try {
|
|
1676
1672
|
return import_fs10.default.existsSync(p);
|
|
@@ -1819,13 +1815,17 @@ async function startDashboard(cwd, opts) {
|
|
|
1819
1815
|
const port = opts.port || 3010;
|
|
1820
1816
|
const outDir = import_path12.default.join(cwd, "aiopt-output");
|
|
1821
1817
|
const file = (name) => import_path12.default.join(outDir, name);
|
|
1818
|
+
let lastCollect = null;
|
|
1819
|
+
let lastCollectError = null;
|
|
1822
1820
|
function ensureUsageFile() {
|
|
1823
1821
|
try {
|
|
1824
1822
|
const usagePath = file("usage.jsonl");
|
|
1825
1823
|
if (import_fs11.default.existsSync(usagePath)) return;
|
|
1826
|
-
const
|
|
1827
|
-
|
|
1828
|
-
|
|
1824
|
+
const r = collectToUsageJsonl(usagePath);
|
|
1825
|
+
lastCollect = { ts: (/* @__PURE__ */ new Date()).toISOString(), outPath: r.outPath, sources: r.sources, eventsWritten: r.eventsWritten };
|
|
1826
|
+
lastCollectError = null;
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
lastCollectError = String(e?.message || e || "collect failed");
|
|
1829
1829
|
}
|
|
1830
1830
|
}
|
|
1831
1831
|
ensureUsageFile();
|
|
@@ -1837,6 +1837,14 @@ async function startDashboard(cwd, opts) {
|
|
|
1837
1837
|
return null;
|
|
1838
1838
|
}
|
|
1839
1839
|
}
|
|
1840
|
+
function statOrNull(p) {
|
|
1841
|
+
try {
|
|
1842
|
+
const st = import_fs11.default.statSync(p);
|
|
1843
|
+
return { size: st.size, mtimeMs: st.mtimeMs };
|
|
1844
|
+
} catch {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1840
1848
|
const indexHtml = `<!doctype html>
|
|
1841
1849
|
<html lang="en">
|
|
1842
1850
|
<head>
|
|
@@ -2096,114 +2104,75 @@ async function load(){
|
|
|
2096
2104
|
document.getElementById('scanMeta').textContent = '(no report.json yet \u2014 run: aiopt scan)';
|
|
2097
2105
|
}
|
|
2098
2106
|
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
const
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
'))
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
// 7d day bin
|
|
2128
|
-
const d = Math.floor((now - t) / 86400000);
|
|
2129
|
-
if(d>=0 && d<7){
|
|
2130
|
-
bins[d].calls++;
|
|
2131
|
-
bins[d].cost += c;
|
|
2132
|
-
}
|
|
2133
|
-
}catch{}
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
// Live sparkline (last 60m)
|
|
2137
|
-
{
|
|
2138
|
-
const W=520, H=120, P=12;
|
|
2139
|
-
const pts = liveBins.slice().reverse();
|
|
2140
|
-
const max = Math.max(...pts.map(b=>b.cost), 0.000001);
|
|
2141
|
-
const xs = pts.map((_,i)=> P + (i*(W-2*P))/59);
|
|
2142
|
-
const ys = pts.map(b=> H-P - ((b.cost/max)*(H-2*P)) );
|
|
2143
|
-
let d = '';
|
|
2144
|
-
for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
|
|
2145
|
-
const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
|
|
2146
|
-
const svg =
|
|
2147
|
-
'<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
|
|
2148
|
-
'<path d="'+area+'" fill="rgba(167,139,250,.10)" />'+
|
|
2149
|
-
'<path d="'+d+'" fill="none" stroke="rgba(167,139,250,.95)" stroke-width="2" />'+
|
|
2150
|
-
'<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max/min '+money(max)+'</text>'+
|
|
2151
|
-
'</svg>';
|
|
2152
|
-
document.getElementById('liveSvg').innerHTML = svg;
|
|
2153
|
-
|
|
2154
|
-
const rows = pts.slice(-10).map((b,idx)=>{
|
|
2155
|
-
const mAgo = 9-idx;
|
|
2156
|
-
const label = (mAgo===0 ? 'now' : (mAgo+'m'));
|
|
2157
|
-
const dollars = ('$' + (Math.round(b.cost*100)/100).toFixed(2));
|
|
2158
|
-
return String(label).padEnd(5) + ' ' + String(dollars).padStart(9) + ' (' + b.calls + ' calls)';
|
|
2159
|
-
});
|
|
2160
|
-
document.getElementById('liveText').textContent = rows.join('
|
|
2107
|
+
// Use computed JSON endpoints to avoid downloading/parsing huge usage.jsonl in the browser.
|
|
2108
|
+
const live60 = await fetch('/api/live-60m.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
|
|
2109
|
+
const sum7d = await fetch('/api/usage-summary.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
|
|
2110
|
+
|
|
2111
|
+
if(live60 && live60.bins){
|
|
2112
|
+
const pts = (live60.bins || []).slice().reverse();
|
|
2113
|
+
const W=520, H=120, P=12;
|
|
2114
|
+
const max = Math.max(...pts.map(b=>Number(b.cost)||0), 0.000001);
|
|
2115
|
+
const xs = pts.map((_,i)=> P + (i*(W-2*P))/59);
|
|
2116
|
+
const ys = pts.map(b=> H-P - (((Number(b.cost)||0)/max)*(H-2*P)) );
|
|
2117
|
+
let d = '';
|
|
2118
|
+
for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
|
|
2119
|
+
const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
|
|
2120
|
+
const svg =
|
|
2121
|
+
'<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
|
|
2122
|
+
'<path d="'+area+'" fill="rgba(167,139,250,.10)" />'+
|
|
2123
|
+
'<path d="'+d+'" fill="none" stroke="rgba(167,139,250,.95)" stroke-width="2" />'+
|
|
2124
|
+
'<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max/min '+money(max)+'</text>'+
|
|
2125
|
+
'</svg>';
|
|
2126
|
+
document.getElementById('liveSvg').innerHTML = svg;
|
|
2127
|
+
|
|
2128
|
+
const rows = pts.slice(-10).map((b,idx)=>{
|
|
2129
|
+
const mAgo = 9-idx;
|
|
2130
|
+
const label = (mAgo===0 ? 'now' : (mAgo+'m'));
|
|
2131
|
+
const dollars = ('$' + (Math.round((Number(b.cost)||0)*100)/100).toFixed(2));
|
|
2132
|
+
return String(label).padEnd(5) + ' ' + String(dollars).padStart(9) + ' (' + (b.calls||0) + ' calls)';
|
|
2133
|
+
});
|
|
2134
|
+
document.getElementById('liveText').textContent = rows.join('
|
|
2161
2135
|
');
|
|
2162
2136
|
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
liveEl.textContent = 'live: on \xB7 last60m ' + money(totalLive);
|
|
2167
|
-
}
|
|
2137
|
+
const liveEl = document.getElementById('live');
|
|
2138
|
+
if(liveEl){
|
|
2139
|
+
liveEl.textContent = 'live: on \xB7 last60m ' + money(live60.totalCostUsd || 0);
|
|
2168
2140
|
}
|
|
2141
|
+
} else {
|
|
2142
|
+
document.getElementById('liveText').textContent = '(no live data yet)';
|
|
2143
|
+
document.getElementById('liveSvg').innerHTML = '';
|
|
2144
|
+
}
|
|
2169
2145
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
'<
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
document.getElementById('trend').textContent = rows.join('
|
|
2146
|
+
if(sum7d && sum7d.dayBins){
|
|
2147
|
+
const bins = sum7d.dayBins || [];
|
|
2148
|
+
const W=520, H=120, P=12;
|
|
2149
|
+
const pts = bins.slice().reverse();
|
|
2150
|
+
const max = Math.max(...pts.map(b=>Number(b.cost)||0), 0.000001);
|
|
2151
|
+
const xs = pts.map((_,i)=> P + (i*(W-2*P))/6);
|
|
2152
|
+
const ys = pts.map(b=> H-P - (((Number(b.cost)||0)/max)*(H-2*P)) );
|
|
2153
|
+
let d = '';
|
|
2154
|
+
for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
|
|
2155
|
+
const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
|
|
2156
|
+
const circles = xs.map((x,i)=>'<circle cx="'+x.toFixed(1)+'" cy="'+ys[i].toFixed(1)+'" r="2.6" fill="rgba(52,211,153,.9)"/>').join('');
|
|
2157
|
+
const svg =
|
|
2158
|
+
'<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
|
|
2159
|
+
'<path d="'+area+'" fill="rgba(96,165,250,.12)" />'+
|
|
2160
|
+
'<path d="'+d+'" fill="none" stroke="rgba(96,165,250,.95)" stroke-width="2" />'+
|
|
2161
|
+
circles+
|
|
2162
|
+
'<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max '+money(max)+'</text>'+
|
|
2163
|
+
'</svg>';
|
|
2164
|
+
document.getElementById('trendSvg').innerHTML = svg;
|
|
2165
|
+
|
|
2166
|
+
const rows = pts.map((b,idx)=>{
|
|
2167
|
+
const label = (idx===pts.length-1 ? 'd-6' : (idx===0 ? 'today' : ('d-'+idx)));
|
|
2168
|
+
const dollars = ('$' + (Math.round((Number(b.cost)||0)*100)/100).toFixed(2));
|
|
2169
|
+
return String(label).padEnd(7) + ' ' + String(dollars).padStart(9) + ' (' + (b.calls||0) + ' calls)';
|
|
2170
|
+
});
|
|
2171
|
+
document.getElementById('trend').textContent = rows.join('
|
|
2197
2172
|
');
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
2173
|
} else {
|
|
2201
|
-
document.getElementById('
|
|
2202
|
-
document.getElementById('liveSvg').innerHTML = '';
|
|
2203
|
-
document.getElementById('trend').textContent = '(no usage.jsonl yet)';
|
|
2174
|
+
document.getElementById('trend').textContent = '(no 7d data yet)';
|
|
2204
2175
|
document.getElementById('trendSvg').innerHTML = '';
|
|
2205
|
-
const liveEl = document.getElementById('live');
|
|
2206
|
-
if(liveEl) liveEl.textContent = 'live: off';
|
|
2207
2176
|
}
|
|
2208
2177
|
|
|
2209
2178
|
const reportMd = await fetch('/api/report.md').then(r=>r.ok?r.text():null).catch(()=>null);
|
|
@@ -2232,16 +2201,77 @@ if(liveEl) liveEl.textContent = 'live: on (polling)';
|
|
|
2232
2201
|
const expected = ["guard-last.txt", "guard-last.json", "report.json", "report.md", "usage.jsonl", "guard-history.jsonl"];
|
|
2233
2202
|
const missing = expected.filter((f) => !import_fs11.default.existsSync(file(f)));
|
|
2234
2203
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
2235
|
-
res.end(JSON.stringify({ baseDir: cwd, outDir, missing }, null, 2));
|
|
2204
|
+
res.end(JSON.stringify({ baseDir: cwd, outDir, missing, collect: lastCollect, collectError: lastCollectError }, null, 2));
|
|
2236
2205
|
return;
|
|
2237
2206
|
}
|
|
2238
|
-
const allow = /* @__PURE__ */ new Set([
|
|
2239
|
-
|
|
2207
|
+
const allow = /* @__PURE__ */ new Set([
|
|
2208
|
+
"guard-last.txt",
|
|
2209
|
+
"guard-last.json",
|
|
2210
|
+
"guard-history.jsonl",
|
|
2211
|
+
"report.md",
|
|
2212
|
+
"report.json",
|
|
2213
|
+
"usage.jsonl",
|
|
2214
|
+
"usage-summary.json",
|
|
2215
|
+
"live-60m.json"
|
|
2216
|
+
]);
|
|
2217
|
+
if (name === "usage.jsonl" || name === "usage-summary.json" || name === "live-60m.json") ensureUsageFile();
|
|
2240
2218
|
if (!allow.has(name)) {
|
|
2241
2219
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
2242
2220
|
res.end("not found");
|
|
2243
2221
|
return;
|
|
2244
2222
|
}
|
|
2223
|
+
if (name === "live-60m.json" || name === "usage-summary.json") {
|
|
2224
|
+
const usagePath = file("usage.jsonl");
|
|
2225
|
+
const st = statOrNull(usagePath);
|
|
2226
|
+
if (!st) {
|
|
2227
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
2228
|
+
res.end("missing");
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
globalThis.__aioptDashCache = globalThis.__aioptDashCache || {};
|
|
2232
|
+
const cache = globalThis.__aioptDashCache;
|
|
2233
|
+
if (!cache.usage || cache.usage.mtimeMs !== st.mtimeMs) {
|
|
2234
|
+
const txt2 = import_fs11.default.readFileSync(usagePath, "utf8");
|
|
2235
|
+
const now = Date.now();
|
|
2236
|
+
const liveBins = Array.from({ length: 60 }, () => ({ cost: 0, calls: 0 }));
|
|
2237
|
+
const dayBins = Array.from({ length: 7 }, () => ({ cost: 0, calls: 0 }));
|
|
2238
|
+
for (const line of txt2.split(/\r?\n/)) {
|
|
2239
|
+
if (!line.trim()) continue;
|
|
2240
|
+
try {
|
|
2241
|
+
const ev = JSON.parse(line);
|
|
2242
|
+
const t = Date.parse(ev.ts);
|
|
2243
|
+
if (!Number.isFinite(t)) continue;
|
|
2244
|
+
const cost = Number(ev.cost_usd);
|
|
2245
|
+
const c = Number.isFinite(cost) ? cost : 0;
|
|
2246
|
+
const dm = Math.floor((now - t) / 6e4);
|
|
2247
|
+
if (dm >= 0 && dm < 60) {
|
|
2248
|
+
liveBins[dm].calls += 1;
|
|
2249
|
+
liveBins[dm].cost += c;
|
|
2250
|
+
}
|
|
2251
|
+
const dd = Math.floor((now - t) / 864e5);
|
|
2252
|
+
if (dd >= 0 && dd < 7) {
|
|
2253
|
+
dayBins[dd].calls += 1;
|
|
2254
|
+
dayBins[dd].cost += c;
|
|
2255
|
+
}
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
cache.usage = {
|
|
2260
|
+
mtimeMs: st.mtimeMs,
|
|
2261
|
+
live60m: {
|
|
2262
|
+
bins: liveBins,
|
|
2263
|
+
totalCostUsd: Math.round(liveBins.reduce((a, b) => a + b.cost, 0) * 100) / 100
|
|
2264
|
+
},
|
|
2265
|
+
summary7d: {
|
|
2266
|
+
dayBins
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
const body = name === "live-60m.json" ? cache.usage.live60m : cache.usage.summary7d;
|
|
2271
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
2272
|
+
res.end(JSON.stringify(body, null, 2));
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2245
2275
|
const p = file(name);
|
|
2246
2276
|
const txt = readOrNull(p);
|
|
2247
2277
|
if (txt === null) {
|
|
@@ -2250,7 +2280,7 @@ if(liveEl) liveEl.textContent = 'live: on (polling)';
|
|
|
2250
2280
|
return;
|
|
2251
2281
|
}
|
|
2252
2282
|
const ct = name.endsWith(".json") ? "application/json; charset=utf-8" : "text/plain; charset=utf-8";
|
|
2253
|
-
res.writeHead(200, { "content-type": ct });
|
|
2283
|
+
res.writeHead(200, { "content-type": ct, "cache-control": "no-store" });
|
|
2254
2284
|
res.end(txt);
|
|
2255
2285
|
return;
|
|
2256
2286
|
}
|
|
@@ -2273,6 +2303,7 @@ var init_dashboard = __esm({
|
|
|
2273
2303
|
import_http = __toESM(require("http"));
|
|
2274
2304
|
import_fs11 = __toESM(require("fs"));
|
|
2275
2305
|
import_path12 = __toESM(require("path"));
|
|
2306
|
+
init_collect();
|
|
2276
2307
|
}
|
|
2277
2308
|
});
|
|
2278
2309
|
|