@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.
Files changed (46) hide show
  1. package/README.md +2 -2
  2. package/app/agents/CostSection.tsx +100 -0
  3. package/app/agents/PageClient.tsx +7 -0
  4. package/app/api/agents/create/route.ts +6 -0
  5. package/app/api/assistant/route.ts +8 -1
  6. package/app/api/costs/agent/route.ts +56 -0
  7. package/app/api/costs/route.ts +19 -77
  8. package/app/api/costs/usage/route.ts +65 -0
  9. package/app/api/gateway/sync.ts +18 -4
  10. package/app/api/stats/route.ts +2 -3
  11. package/app/api/stats-since/route.ts +1 -1
  12. package/app/api/stream/route.ts +3 -5
  13. package/app/api/system-health/route.ts +33 -32
  14. package/app/components/CostBreakdown.tsx +1 -1
  15. package/app/components/Sidebar.tsx +10 -0
  16. package/app/components/SystemCockpit.tsx +1 -1
  17. package/app/components/ui/TimeSeriesChart.tsx +27 -9
  18. package/app/costs/CostsSkeleton.tsx +88 -0
  19. package/app/costs/PageClient.tsx +366 -0
  20. package/app/costs/loading.tsx +13 -0
  21. package/app/costs/page.tsx +5 -0
  22. package/app/globals.css +6 -0
  23. package/daemon.js +455 -41
  24. package/docs/ARCHITECTURE.md +39 -29
  25. package/docs/DESIGN-SYSTEM.md +2 -2
  26. package/docs/FRONTEND-ARCHITECTURE.md +4 -2
  27. package/docs/REV4A.md +4 -2
  28. package/docs/dev/API-REFERENCE.md +118 -22
  29. package/docs/dev/DATABASE.md +119 -19
  30. package/docs/dev/GATEWAY.md +53 -9
  31. package/docs/rag/DATA-FRESHNESS.md +19 -7
  32. package/docs/rag/GLOSSARY.md +7 -4
  33. package/docs/rag/REV4A-OVERVIEW.md +4 -1
  34. package/docs/rag/WHAT-I-CAN-ANSWER.md +3 -3
  35. package/instrumentation.ts +11 -0
  36. package/lib/agent-costs.ts +172 -0
  37. package/lib/agent-recreate.ts +3 -0
  38. package/lib/cost-reconciliation.ts +78 -0
  39. package/lib/costs-db.ts +31 -0
  40. package/lib/model-pricing.ts +123 -3
  41. package/lib/price-schedule-sync.ts +133 -0
  42. package/lib/utils/format.ts +12 -0
  43. package/model-pricing.json +269 -121
  44. package/package.json +1 -1
  45. package/scripts/refresh-model-pricing.mjs +16 -4
  46. 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
+ }
@@ -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);