@krtclcdy/tersio 2.20.1 → 2.22.0
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/README.md +8 -9
- package/cli/common.js +14 -1
- package/cli/dashboard.js +367 -35
- package/cli/doctor-fix.js +185 -0
- package/cli/doctor.js +120 -53
- package/cli/install.js +46 -60
- package/cli/rtk-wiring.js +1 -1
- package/cli/settings.js +128 -5
- package/cli/uninstall.js +4 -4
- package/cli/update.js +29 -20
- package/cli/usage.js +1 -1
- package/dashboard/{app.js → charts.js} +74 -361
- package/dashboard/core.js +357 -0
- package/dashboard/settings.js +166 -0
- package/dashboard/share.js +218 -0
- package/dashboard/styles.css +87 -12
- package/dashboard/template.html +162 -57
- package/extensions/ai-addons-updater/index.js +13 -30
- package/extensions/ai-addons-updater/index.ts +11 -29
- package/extensions/lib/utils.js +26 -1
- package/extensions/lib/utils.ts +28 -1
- package/extensions/shared/pricing.js +2 -2
- package/extensions/shared/pricing.ts +2 -1
- package/extensions/shared/usage-ledger.js +3 -2
- package/extensions/shared/usage-ledger.ts +3 -2
- package/extensions/shared/usage-store.js +7 -7
- package/extensions/shared/usage-store.ts +7 -6
- package/gain/dist/brand.webp +0 -0
- package/gain/dist/index.html +65 -0
- package/gain/dist/vite.svg +1 -0
- package/package.json +17 -34
- package/tersio.js +3 -2
package/cli/settings.js
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// override flags and --currency). Interactive: clack selects seeded from the
|
|
5
5
|
// stored profile.
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, } from './common.js';
|
|
7
|
+
import { CAVEMAN_DEFAULTS, COMBO_DEFAULTS, COMBO_PRESET_MODES, OMP_PLUGINS_DIR, PACKAGE_NAME, PONYTAIL_DEFAULTS, cavemanDefaultFlag, comboDefaultFlag, currency, currencyGiven, diagScheduleFlag, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, settingArg, } from './common.js';
|
|
8
8
|
import { CURRENCY_CODES } from './currency.js';
|
|
9
9
|
import { textTable } from './usage.js';
|
|
10
10
|
import { askInteractiveChoice, closeRL, tty } from './interactive.js';
|
|
11
11
|
import { formatProfile, storedProfile, writePluginSettings } from './profile.js';
|
|
12
|
+
import { readDiagSchedule, setDiagSchedule } from './dashboard.js';
|
|
12
13
|
function applyFlags(base) {
|
|
13
14
|
const next = { ...base };
|
|
14
15
|
if (comboDefaultFlag !== undefined)
|
|
@@ -71,6 +72,94 @@ async function askProfile(current) {
|
|
|
71
72
|
next.currency = cur.value;
|
|
72
73
|
return next;
|
|
73
74
|
}
|
|
75
|
+
async function askDiagSchedule(current) {
|
|
76
|
+
const picked = await askInteractiveChoice('Diagnosis auto-check', [
|
|
77
|
+
{ value: 'manual', label: 'manual', hint: 'check on open' },
|
|
78
|
+
{ value: 'daily', label: 'daily' },
|
|
79
|
+
{ value: 'weekly', label: 'weekly' },
|
|
80
|
+
{ value: 'monthly', label: 'monthly' },
|
|
81
|
+
], current);
|
|
82
|
+
if (picked.status !== 'selected')
|
|
83
|
+
return null;
|
|
84
|
+
return picked.value;
|
|
85
|
+
}
|
|
86
|
+
const SETTING_NAMES = ['combo', 'caveman', 'rtk', 'ponytail', 'currency', 'diagnosis'];
|
|
87
|
+
function settingsUsage() {
|
|
88
|
+
console.log(' Usage: tersio settings [combo|caveman|rtk|ponytail|currency|diagnosis] [--combo-default off|medium|balanced|max] [--caveman-default off|lite|full|ultra|wenyan] [--rtk-default on|off] [--ponytail-default off|lite|full|ultra|review] [--currency USD|PHP|EUR|GBP|JPY|KRW|SGD|AUD|CAD|INR] [--diag-schedule manual|daily|weekly|monthly] [--dry-run]');
|
|
89
|
+
}
|
|
90
|
+
// Single-setting jump: `tersio settings diagnosis` prompts only that value
|
|
91
|
+
// instead of walking the whole chain. Returns false when aborted.
|
|
92
|
+
async function runSingleSetting(name, current, nextDiag) {
|
|
93
|
+
const next = { ...current };
|
|
94
|
+
let diag = nextDiag;
|
|
95
|
+
switch (name) {
|
|
96
|
+
case 'combo': {
|
|
97
|
+
const combo = await askInteractiveChoice('Session-start defaults — Combo preset', [
|
|
98
|
+
{ value: 'off', label: 'off' },
|
|
99
|
+
{ value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
|
|
100
|
+
{ value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
|
|
101
|
+
{ value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
|
|
102
|
+
], current.comboDefault);
|
|
103
|
+
if (combo.status !== 'selected')
|
|
104
|
+
return null;
|
|
105
|
+
const preset = COMBO_PRESET_MODES[combo.value] ?? COMBO_PRESET_MODES.off;
|
|
106
|
+
next.comboDefault = combo.value;
|
|
107
|
+
next.cavemanDefault = preset.caveman;
|
|
108
|
+
next.rtkDefault = preset.rtk;
|
|
109
|
+
next.ponytailDefault = preset.ponytail;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'caveman': {
|
|
113
|
+
const picked = await askInteractiveChoice('Caveman default', [...CAVEMAN_DEFAULTS].map((v) => ({
|
|
114
|
+
value: v, label: v,
|
|
115
|
+
})), next.cavemanDefault);
|
|
116
|
+
if (picked.status !== 'selected')
|
|
117
|
+
return null;
|
|
118
|
+
next.cavemanDefault = picked.value;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'rtk': {
|
|
122
|
+
const picked = await askInteractiveChoice('RTK default', [
|
|
123
|
+
{ value: 'on', label: 'on' },
|
|
124
|
+
{ value: 'off', label: 'off' },
|
|
125
|
+
], next.rtkDefault ? 'on' : 'off');
|
|
126
|
+
if (picked.status !== 'selected')
|
|
127
|
+
return null;
|
|
128
|
+
next.rtkDefault = picked.value === 'on';
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'ponytail': {
|
|
132
|
+
const picked = await askInteractiveChoice('Ponytail default', [...PONYTAIL_DEFAULTS].map((v) => ({
|
|
133
|
+
value: v, label: v,
|
|
134
|
+
})), next.ponytailDefault);
|
|
135
|
+
if (picked.status !== 'selected')
|
|
136
|
+
return null;
|
|
137
|
+
next.ponytailDefault = picked.value;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'currency': {
|
|
141
|
+
const picked = await askInteractiveChoice('Display currency (usage/gain reports)', CURRENCY_CODES.map((v) => ({
|
|
142
|
+
value: v, label: v,
|
|
143
|
+
})), next.currency);
|
|
144
|
+
if (picked.status !== 'selected')
|
|
145
|
+
return null;
|
|
146
|
+
next.currency = picked.value;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'diagnosis': {
|
|
150
|
+
const picked = await askDiagSchedule(diag);
|
|
151
|
+
if (picked === null)
|
|
152
|
+
return null;
|
|
153
|
+
diag = picked;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
default: {
|
|
157
|
+
console.error(`[fail] Invalid setting: ${name}. Valid: ${SETTING_NAMES.join(', ')}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { profile: next, diag };
|
|
162
|
+
}
|
|
74
163
|
function printSettingsTable(current) {
|
|
75
164
|
console.log('\n=== Tersio Settings ===');
|
|
76
165
|
// Currency lives here no longer: the gain dashboard owns displaying and
|
|
@@ -81,6 +170,7 @@ function printSettingsTable(current) {
|
|
|
81
170
|
['caveman', current.cavemanDefault, [...CAVEMAN_DEFAULTS].join(' | ')],
|
|
82
171
|
['rtk', current.rtkDefault ? 'on' : 'off', 'on | off'],
|
|
83
172
|
['ponytail', current.ponytailDefault, [...PONYTAIL_DEFAULTS].join(' | ')],
|
|
173
|
+
['diagnosis', readDiagSchedule(), 'manual | daily | weekly | monthly'],
|
|
84
174
|
];
|
|
85
175
|
for (const l of textTable(['Setting', 'Current', 'Valid values'], rows, [false, false, false], 64)) {
|
|
86
176
|
console.log(l);
|
|
@@ -91,7 +181,32 @@ async function runSettings() {
|
|
|
91
181
|
const current = await storedProfile();
|
|
92
182
|
printSettingsTable(current);
|
|
93
183
|
let next;
|
|
94
|
-
|
|
184
|
+
let nextDiag = readDiagSchedule();
|
|
185
|
+
if (diagScheduleFlag !== undefined)
|
|
186
|
+
nextDiag = diagScheduleFlag;
|
|
187
|
+
if (settingArg !== null) {
|
|
188
|
+
if (!SETTING_NAMES.includes(settingArg)) {
|
|
189
|
+
console.error(`[fail] Invalid setting: ${settingArg}. Valid: ${SETTING_NAMES.join(', ')}`);
|
|
190
|
+
closeRL();
|
|
191
|
+
process.exit(1);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!tty()) {
|
|
195
|
+
console.log('\n Single-setting jump needs a terminal.');
|
|
196
|
+
settingsUsage();
|
|
197
|
+
closeRL();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const single = await runSingleSetting(settingArg, current, nextDiag);
|
|
201
|
+
if (single === null) {
|
|
202
|
+
closeRL();
|
|
203
|
+
process.exit(130);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
next = single.profile;
|
|
207
|
+
nextDiag = single.diag;
|
|
208
|
+
}
|
|
209
|
+
else if (profileFlagsGiven || currencyGiven || diagScheduleFlag !== undefined) {
|
|
95
210
|
next = applyFlags(current);
|
|
96
211
|
}
|
|
97
212
|
else if (tty()) {
|
|
@@ -101,20 +216,28 @@ async function runSettings() {
|
|
|
101
216
|
process.exit(130);
|
|
102
217
|
return;
|
|
103
218
|
}
|
|
219
|
+
const picked = await askDiagSchedule(nextDiag);
|
|
220
|
+
if (picked === null) {
|
|
221
|
+
closeRL();
|
|
222
|
+
process.exit(130);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
nextDiag = picked;
|
|
104
226
|
}
|
|
105
227
|
else {
|
|
106
228
|
console.log('\n No flags given and no TTY — nothing to change.');
|
|
107
|
-
|
|
229
|
+
settingsUsage();
|
|
108
230
|
closeRL();
|
|
109
231
|
return;
|
|
110
232
|
}
|
|
111
233
|
if (dryRun) {
|
|
112
|
-
console.log(`\n [dry-run] would set defaults: ${formatProfile(next)}`);
|
|
234
|
+
console.log(`\n [dry-run] would set defaults: ${formatProfile(next)} · diagnosis=${nextDiag}`);
|
|
113
235
|
closeRL();
|
|
114
236
|
return;
|
|
115
237
|
}
|
|
116
238
|
await writePluginSettings(next, {});
|
|
117
|
-
|
|
239
|
+
setDiagSchedule(nextDiag);
|
|
240
|
+
console.log(` Defaults: ${formatProfile(next)} · diagnosis=${nextDiag}`);
|
|
118
241
|
console.log(' Applies to fresh sessions only — anything persisted with /combo, /caveman, or /rtk wins.');
|
|
119
242
|
console.log(' Currency applies to usage/gain reports; --currency overrides it per run.');
|
|
120
243
|
closeRL();
|
package/cli/uninstall.js
CHANGED
|
@@ -56,8 +56,8 @@ async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
|
|
|
56
56
|
async function runUninstall(options = {}) {
|
|
57
57
|
const shouldDryRun = options.dryRun ?? dryRun;
|
|
58
58
|
const confirmed = (options.yes ?? yes) || shouldDryRun;
|
|
59
|
-
// Ponytail
|
|
60
|
-
// --keep-ponytail opts out and reinstall always preserves it.
|
|
59
|
+
// Ponytail is bundled with Tersio's presets, so a full uninstall removes
|
|
60
|
+
// its copy too; --keep-ponytail opts out and reinstall always preserves it.
|
|
61
61
|
const shouldRemovePonytail = options.removePonytail ?? (removePonytail || !keepPonytail);
|
|
62
62
|
const shouldRemoveRtk = options.removeRtk ?? removeRtk;
|
|
63
63
|
console.log('\n=== Tersio Uninstall ===\n');
|
|
@@ -135,8 +135,8 @@ async function runUninstall(options = {}) {
|
|
|
135
135
|
await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
// Remove the Ponytail
|
|
139
|
-
//
|
|
138
|
+
// Remove the bundled Ponytail copy (dep entry exists only on
|
|
139
|
+
// pre-bundle installs); the config.yml entry was filtered above.
|
|
140
140
|
if (shouldRemovePonytail) {
|
|
141
141
|
const pluginsPkgPath = path.join(pluginsDir, 'package.json');
|
|
142
142
|
await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
|
package/cli/update.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { BUN_BIN_DIR, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, dryRun, execP, parseJsonObject, verbose, } from './common.js';
|
|
6
6
|
import { execNetwork } from './interactive.js';
|
|
7
|
-
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
7
|
+
import { readTextIfExists, tersioDataPath } from '../extensions/lib/utils.js';
|
|
8
8
|
import { refreshPrices } from '../extensions/shared/pricing.js';
|
|
9
9
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
|
|
10
10
|
const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -39,7 +39,7 @@ async function latestPublishedVersion() {
|
|
|
39
39
|
// (cached under OMP_PLUGINS_DIR) unless force skips the cache — an explicit
|
|
40
40
|
// "check for updates" must never trust a stale cache.
|
|
41
41
|
async function checkForUpdate(force = false) {
|
|
42
|
-
const cachePath =
|
|
42
|
+
const cachePath = tersioDataPath('update-check.json', 'tersio-update-check.json');
|
|
43
43
|
const cached = parseJsonObject(await readTextIfExists(cachePath));
|
|
44
44
|
const cacheFresh = typeof cached?.lastCheck === 'number' && Date.now() - cached.lastCheck < UPDATE_CHECK_TTL_MS;
|
|
45
45
|
if (!force && cacheFresh && cached?.latest)
|
|
@@ -68,20 +68,6 @@ async function settle(work, ms) {
|
|
|
68
68
|
clearTimeout(timer);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
// Plain `npm view <spec> version` with no spinner, for background probes.
|
|
72
|
-
async function npmViewVersion(spec, timeoutMs) {
|
|
73
|
-
try {
|
|
74
|
-
const exe = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
75
|
-
const args = IS_WINDOWS
|
|
76
|
-
? ['/d', '/s', '/c', 'npm', 'view', spec, 'version', '--prefer-online']
|
|
77
|
-
: ['view', spec, 'version', '--prefer-online'];
|
|
78
|
-
const r = await execP(exe, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024, windowsHide: true, shell: false });
|
|
79
|
-
return r.stdout.trim() || null;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
71
|
// Run a child with inherited stdio for live output. Used when the child
|
|
86
72
|
// renders its own interactive UI (the delegated installer runs Clack
|
|
87
73
|
// spinners), which a concurrent outer spinner would corrupt into stray bars.
|
|
@@ -100,20 +86,19 @@ async function probeUpdatePlan(cliLatest) {
|
|
|
100
86
|
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
101
87
|
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
102
88
|
const ruleDest = path.join(OMP_AGENT_DIR, 'extensions', 'caveman-session', 'rule.md');
|
|
103
|
-
const [rtkLocal, rtkRelease, ruleLocalText, ruleRemoteText, ponytailLocalText
|
|
89
|
+
const [rtkLocal, rtkRelease, ruleLocalText, ruleRemoteText, ponytailLocalText] = await Promise.all([
|
|
104
90
|
settle(execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null), 6000),
|
|
105
91
|
settle(fetchJson(RTK_RELEASE_API).then((r) => normalizeRtkVersion(r.tag_name) || null), 4000),
|
|
106
92
|
readTextIfExists(ruleDest),
|
|
107
93
|
settle(httpsGet(CAVEMAN_REMOTE_RULE), 4000),
|
|
108
94
|
readTextIfExists(ponytailPkg),
|
|
109
|
-
settle(npmViewVersion('@dietrichgebert/ponytail', 4000), 5000),
|
|
110
95
|
]);
|
|
111
96
|
const shortHash = (text) => text === null ? null : sha256Hex(text).slice(0, 8);
|
|
112
97
|
return {
|
|
113
98
|
cli: cliLatest,
|
|
114
99
|
rtk: [rtkLocal, rtkRelease],
|
|
115
100
|
rule: [shortHash(ruleLocalText), shortHash(ruleRemoteText)],
|
|
116
|
-
ponytail:
|
|
101
|
+
ponytail: ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null,
|
|
117
102
|
};
|
|
118
103
|
}
|
|
119
104
|
// Every add-on prints its status: version jump when stale, "up to date"
|
|
@@ -136,7 +121,12 @@ function planLines(plan) {
|
|
|
136
121
|
push('Tersio', PACKAGE_VERSION, plan.cli);
|
|
137
122
|
push('RTK', plan.rtk[0], plan.rtk[1]);
|
|
138
123
|
push('Caveman rule', plan.rule[0], plan.rule[1]);
|
|
139
|
-
|
|
124
|
+
if (plan.ponytail)
|
|
125
|
+
status.push(`Ponytail: ${plan.ponytail} (bundled with tersio)`);
|
|
126
|
+
else {
|
|
127
|
+
status.push('Ponytail: missing → bundled reinstall');
|
|
128
|
+
stale.push('Ponytail: missing → bundled reinstall');
|
|
129
|
+
}
|
|
140
130
|
return { stale, status };
|
|
141
131
|
}
|
|
142
132
|
async function runLatestUpdate() {
|
|
@@ -166,6 +156,25 @@ async function runLatestUpdate() {
|
|
|
166
156
|
return;
|
|
167
157
|
}
|
|
168
158
|
console.log(`Updating ${stale.join(', ')}`);
|
|
159
|
+
// Broken payloads (v2.20.0) crash on boot — smoke-check the target before touching anything installed.
|
|
160
|
+
const smokeArgs = IS_WINDOWS
|
|
161
|
+
? ['/d', '/s', '/c', 'npm', 'exec', '--yes', '--prefer-online', `--package=${PACKAGE_NAME}${target}`, '--', PACKAGE_BIN, '--version']
|
|
162
|
+
: ['exec', '--yes', '--prefer-online', `--package=${PACKAGE_NAME}${target}`, '--', PACKAGE_BIN, '--version'];
|
|
163
|
+
try {
|
|
164
|
+
await execP(npmCommand, smokeArgs, {
|
|
165
|
+
timeout: 120000,
|
|
166
|
+
maxBuffer: 1024 * 1024,
|
|
167
|
+
windowsHide: true,
|
|
168
|
+
shell: false,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
console.error(`[fail] Update payload ${PACKAGE_NAME}${target} failed its smoke check — staying on ${PACKAGE_VERSION}.`);
|
|
173
|
+
console.error(`[fail] ${e.message.split('\n')[0]}`);
|
|
174
|
+
console.error(`[hint] Retry once released fixed; manual: npm install -g ${PACKAGE_NAME}@latest --no-audit --no-fund --prefer-online`);
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
169
178
|
// The npx delegation below only refreshes the OMP-side files; the globally
|
|
170
179
|
// installed CLI keeps its old version until npm -g runs. Refresh both.
|
|
171
180
|
const globalArgs = IS_WINDOWS
|
package/cli/usage.js
CHANGED
|
@@ -132,7 +132,7 @@ function printReport(report) {
|
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
const t = report.tokens;
|
|
135
|
-
console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read
|
|
135
|
+
console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`);
|
|
136
136
|
console.log(` COST ${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)`);
|
|
137
137
|
const models = Object.entries(report.byModel).filter(([, b]) => b.input + b.output + b.cacheRead + b.cacheWrite > 0).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output)).slice(0, 8);
|
|
138
138
|
if (models.length) {
|