@flame0510/project-aether 1.9.0 → 1.10.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 +2 -2
- package/app/agents/CostSection.tsx +100 -0
- package/app/agents/PageClient.tsx +7 -0
- package/app/api/agents/create/route.ts +6 -0
- package/app/api/assistant/route.ts +8 -1
- package/app/api/costs/agent/route.ts +56 -0
- package/app/api/costs/route.ts +19 -77
- package/app/api/costs/usage/route.ts +65 -0
- package/app/api/gateway/sync.ts +18 -4
- package/app/api/stats/route.ts +2 -3
- package/app/api/stats-since/route.ts +1 -1
- package/app/api/stream/route.ts +3 -5
- package/app/api/system-health/route.ts +33 -32
- package/app/components/CostBreakdown.tsx +1 -1
- package/app/components/Sidebar.tsx +10 -0
- package/app/components/SystemCockpit.tsx +1 -1
- package/app/components/ui/TimeSeriesChart.tsx +27 -9
- package/app/costs/CostsSkeleton.tsx +88 -0
- package/app/costs/PageClient.tsx +366 -0
- package/app/costs/loading.tsx +13 -0
- package/app/costs/page.tsx +5 -0
- package/app/globals.css +6 -0
- package/daemon.js +455 -41
- package/docs/ARCHITECTURE.md +39 -29
- package/docs/DESIGN-SYSTEM.md +2 -2
- package/docs/FRONTEND-ARCHITECTURE.md +4 -2
- package/docs/REV4A.md +4 -2
- package/docs/dev/API-REFERENCE.md +118 -22
- package/docs/dev/DATABASE.md +119 -19
- package/docs/dev/GATEWAY.md +53 -9
- package/docs/rag/DATA-FRESHNESS.md +19 -7
- package/docs/rag/GLOSSARY.md +7 -4
- package/docs/rag/REV4A-OVERVIEW.md +4 -1
- package/docs/rag/WHAT-I-CAN-ANSWER.md +3 -3
- package/instrumentation.ts +11 -0
- package/lib/agent-costs.ts +172 -0
- package/lib/agent-recreate.ts +3 -0
- package/lib/cost-reconciliation.ts +78 -0
- package/lib/costs-db.ts +31 -0
- package/lib/model-pricing.ts +123 -3
- package/lib/price-schedule-sync.ts +133 -0
- package/lib/utils/format.ts +12 -0
- package/model-pricing.json +269 -121
- package/package.json +1 -1
- package/scripts/refresh-model-pricing.mjs +16 -4
- package/lib/billing.ts +0 -100
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-sync the agents' prices when a vendor's rate changes with the time of day.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw's `cost` has no notion of time: it prices each call with the rates in its
|
|
5
|
+
* config at that moment and keeps that cost. DeepSeek bills half price off-peak, so at
|
|
6
|
+
* every change of band the `rev4a` block is written again with the rates now in force
|
|
7
|
+
* (`priceAt()` in `buildRev4aProviderConfig()`). A config patch of `models` applies
|
|
8
|
+
* without a gateway restart.
|
|
9
|
+
*
|
|
10
|
+
* A check, not one long timer: at most every minute it compares the bands in force with
|
|
11
|
+
* the ones last written, and syncs only when they differ. A single timeout to the next
|
|
12
|
+
* change would fire late after the machine sleeps (a laptop), leaving every agent on
|
|
13
|
+
* the other band's rates; the check costs a little arithmetic and no I/O. When the next
|
|
14
|
+
* change is less than a minute away the check is set for that moment, so a change is
|
|
15
|
+
* applied within a couple of seconds.
|
|
16
|
+
*
|
|
17
|
+
* The check runs whether or not a scheduled model is on offer, and skips the sync when
|
|
18
|
+
* none is: enabling a DeepSeek model later needs no restart to be priced right. A sync
|
|
19
|
+
* that did not reach every agent is retried every few minutes, not every check — a full
|
|
20
|
+
* sync holds the server for a few seconds — and at most a few times: an agent busy with
|
|
21
|
+
* an update, recreate, restore or edit writes the prices itself when it finishes, and one
|
|
22
|
+
* that stays broken must not keep the server syncing every agent around the clock. A new
|
|
23
|
+
* change of band is applied at once, whatever retry was pending.
|
|
24
|
+
*/
|
|
25
|
+
import { syncAllAgents } from '@/app/api/gateway/sync';
|
|
26
|
+
import { loadModelsConfig, loadOfferedModels } from '@/lib/model-catalogue';
|
|
27
|
+
import { PRICE_SCHEDULES, nextPriceChange, priceBandAt, priceScheduleFor } from '@/lib/model-pricing';
|
|
28
|
+
|
|
29
|
+
const CHECK_MS = 60_000;
|
|
30
|
+
/** Past the hour mark, so the band has changed when the sync reads the clock. */
|
|
31
|
+
const AFTER_CHANGE_MS = 2_000;
|
|
32
|
+
const RETRY_MS = 5 * 60_000;
|
|
33
|
+
/** Syncs tried for one band before waiting for the next change (or the next save or restart). */
|
|
34
|
+
const MAX_TRIES = 3;
|
|
35
|
+
|
|
36
|
+
interface State {
|
|
37
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
38
|
+
/**
|
|
39
|
+
* The bands last written to the agents, e.g. `deepseek:off-peak`. Set by every sync,
|
|
40
|
+
* complete or not: a sync that misses one agent still writes every other one.
|
|
41
|
+
*/
|
|
42
|
+
applied?: string;
|
|
43
|
+
/** Syncs tried for `applied` that missed an agent. */
|
|
44
|
+
tries?: number;
|
|
45
|
+
/** When to retry for the agents the last sync missed; unset when nothing is owed. */
|
|
46
|
+
retryAfter?: number;
|
|
47
|
+
}
|
|
48
|
+
// One timer per process, across dev-mode module reloads.
|
|
49
|
+
const g = globalThis as unknown as { __rev4aPriceSchedule?: State };
|
|
50
|
+
const state: State = (g.__rev4aPriceSchedule ??= {});
|
|
51
|
+
|
|
52
|
+
/** The band of every scheduled vendor right now, as one comparable string. */
|
|
53
|
+
function bandsNow(at: Date): string {
|
|
54
|
+
return Object.keys(PRICE_SCHEDULES)
|
|
55
|
+
.map((vendor) => `${vendor}:${priceBandAt(`${vendor}/`, at)}`)
|
|
56
|
+
.join(',');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function schedule(): void {
|
|
60
|
+
if (state.timer) clearTimeout(state.timer);
|
|
61
|
+
const next = nextPriceChange(loadModelsConfig().map((m) => m.id));
|
|
62
|
+
const untilChange = next ? next.getTime() - Date.now() + AFTER_CHANGE_MS : Infinity;
|
|
63
|
+
state.timer = setTimeout(check, Math.max(1_000, Math.min(CHECK_MS, untilChange)));
|
|
64
|
+
state.timer.unref?.();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Write `bands` into every agent; on a miss, owe a retry until MAX_TRIES. */
|
|
68
|
+
function sync(bands: string): void {
|
|
69
|
+
let ok = false;
|
|
70
|
+
let summary = '';
|
|
71
|
+
try {
|
|
72
|
+
const outcome = syncAllAgents();
|
|
73
|
+
ok = outcome.ok;
|
|
74
|
+
summary = outcome.summary;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
summary = (e as Error).message;
|
|
77
|
+
}
|
|
78
|
+
state.applied = bands;
|
|
79
|
+
if (ok) {
|
|
80
|
+
state.tries = 0;
|
|
81
|
+
state.retryAfter = undefined;
|
|
82
|
+
console.log(`[rev4a] Price band now ${bands}: ${summary}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
state.tries = (state.tries ?? 0) + 1;
|
|
86
|
+
if (state.tries < MAX_TRIES) {
|
|
87
|
+
state.retryAfter = Date.now() + RETRY_MS;
|
|
88
|
+
console.warn(`[rev4a] Price band now ${bands}, sync incomplete: ${summary} Retrying in ${RETRY_MS / 60_000} min.`);
|
|
89
|
+
} else {
|
|
90
|
+
state.retryAfter = undefined;
|
|
91
|
+
console.warn(`[rev4a] Price band now ${bands}, sync incomplete after ${state.tries} tries: ${summary} Not retried until the next change; Sync All Agents applies it sooner.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function check(): void {
|
|
96
|
+
try {
|
|
97
|
+
const bands = bandsNow(new Date());
|
|
98
|
+
const offered = loadOfferedModels().some((m) => priceScheduleFor(m.id));
|
|
99
|
+
if (bands !== state.applied) {
|
|
100
|
+
// A change of band: write it now, whatever retry was owed for the previous one.
|
|
101
|
+
state.tries = 0;
|
|
102
|
+
state.retryAfter = undefined;
|
|
103
|
+
if (offered) sync(bands);
|
|
104
|
+
else state.applied = bands; // nothing on offer is priced by time of day
|
|
105
|
+
} else if (state.retryAfter && Date.now() >= state.retryAfter) {
|
|
106
|
+
if (offered) sync(bands);
|
|
107
|
+
else state.retryAfter = undefined;
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.warn(`[rev4a] Price band check failed: ${(e as Error).message}`);
|
|
111
|
+
} finally {
|
|
112
|
+
try {
|
|
113
|
+
schedule();
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// Never let the chain die: check again in a minute.
|
|
116
|
+
console.warn(`[rev4a] Price band check could not be scheduled: ${(e as Error).message}`);
|
|
117
|
+
state.timer = setTimeout(check, CHECK_MS);
|
|
118
|
+
state.timer.unref?.();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Start the checks; called once at server start, after the startup sync. When that sync
|
|
125
|
+
* reached every agent, the bands it wrote count as applied; otherwise the first check
|
|
126
|
+
* (a minute later) writes them.
|
|
127
|
+
*/
|
|
128
|
+
export function startPriceScheduleSync(startupSynced: boolean): void {
|
|
129
|
+
state.applied = startupSynced ? bandsNow(new Date()) : undefined;
|
|
130
|
+
state.tries = 0;
|
|
131
|
+
state.retryAfter = undefined;
|
|
132
|
+
schedule();
|
|
133
|
+
}
|
package/lib/utils/format.ts
CHANGED
|
@@ -4,6 +4,18 @@ export function formatUsd(value: number | null | undefined): string {
|
|
|
4
4
|
return `$${Number(value ?? 0).toFixed(2)}`;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Dollars with the precision a small figure needs: an agent's day can be a fraction of a
|
|
9
|
+
* cent, which formatUsd would show as $0.00. Used wherever agent costs are shown.
|
|
10
|
+
*/
|
|
11
|
+
export function formatCost(value: number | null | undefined): string {
|
|
12
|
+
const v = Number(value ?? 0);
|
|
13
|
+
if (!v) return '$0.00';
|
|
14
|
+
if (v < 0.01) return `${v.toFixed(4)}`;
|
|
15
|
+
if (v < 1) return `${v.toFixed(3)}`;
|
|
16
|
+
return `${v.toFixed(2)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export function formatUsdOrDash(value: number | null | undefined): string {
|
|
8
20
|
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '-';
|
|
9
21
|
return formatUsd(value);
|