@flame0510/project-aether 1.11.0 → 1.11.1

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.
@@ -1,6 +1,6 @@
1
1
  # Rev4a API Reference
2
2
 
3
- > **Last updated:** 2026-10-01
3
+ > **Last updated:** 2026-10-02
4
4
 
5
5
  All routes are under `/api/`. Authentication is required on every endpoint
6
6
  unless otherwise noted.
@@ -251,10 +251,15 @@ Returns current provider configuration state.
251
251
 
252
252
  **Response:** each provider carries its models, and a model carries `params` (the total
253
253
  parameter count) when its Hugging Face card publishes one — the Gateway row shows it next
254
- to the price, and a model without a card simply omits it.
254
+ to the price, and a model without a card simply omits it. A model priced by time of day
255
+ carries `band` (`"peak"` or `"off-peak"`, what the vendor bills right now — the Gateway row
256
+ shows it as a chip) and its `pricing` is the rate in force at answer time, not the peak
257
+ list price: it flips with the band, like the agents' synced prices. The top level carries
258
+ `nextPriceChangeMs`, when the band flips next, so an open page can refresh the chips.
255
259
 
256
260
  ```json
257
261
  {
262
+ "nextPriceChangeMs": 1790532000000,
258
263
  "providers": [
259
264
  {
260
265
  "provider": "deepseek",
@@ -263,8 +268,8 @@ to the price, and a model without a card simply omits it.
263
268
  "baseUrl": "https://api.deepseek.com",
264
269
  "docsUrl": "https://platform.deepseek.com/api_keys",
265
270
  "models": [
266
- { "id": "deepseek/deepseek-flash", "name": "DeepSeek Flash", "enabled": true },
267
- { "id": "deepseek/deepseek-v4-pro", "name": "DeepSeek V4 Pro", "enabled": true }
271
+ { "id": "deepseek/deepseek-flash", "name": "DeepSeek Flash", "enabled": true, "band": "off-peak", "pricing": { "input": 0.15, "output": 0.6, "cacheRead": 0.003, "cacheWrite": 0.15 } },
272
+ { "id": "deepseek/deepseek-v4-pro", "name": "DeepSeek V4 Pro", "enabled": true, "band": "off-peak", "pricing": { "input": 0.66, "output": 1.98, "cacheRead": 0.022, "cacheWrite": 0.66 } }
268
273
  ]
269
274
  }
270
275
  ]
@@ -320,8 +325,8 @@ Rev4a Provider Gateway — proxy chat completions to the correct upstream.
320
325
  ---
321
326
 
322
327
  ### `GET /api/models/details?id=<model id>`
323
- Everything the details modal shows for one model, and nothing more: fields the sources do
324
- not have are **absent**, never `null` and never a guess.
328
+ Everything the details modal shows for one model, and nothing more: unknown optional
329
+ facts are omitted; `price` and `info` can be `null` when absent.
325
330
 
326
331
  **Auth:** browser cookie or bearer token
327
332
 
@@ -330,7 +335,7 @@ not have are **absent**, never `null` and never a guess.
330
335
  {
331
336
  "id": "glm/glm-5.3-flash", "name": "GLM-5.3 Flash", "provider": "glm",
332
337
  "modality": "text+image+video->text", "enabled": false, "deprecated": false,
333
- "price": { "input": 0.15, "output": 0.5, "source": "vendor" },
338
+ "price": { "input": 0.15, "output": 0.5, "source": "vendor", "scheduled": false },
334
339
  "details": { "description": "…", "created": "2026-08-26", "context": 1310720,
335
340
  "providerContext": 1048576, "maxOutput": 943718,
336
341
  "reasoning": { "mandatory": true, "default_effort": "max", "supported_efforts": ["max","high","low"] },
@@ -338,13 +343,15 @@ not have are **absent**, never `null` and never a guess.
338
343
  "benchmarks": { "design_arena": [ … ], "artificial_analysis": { … } },
339
344
  "huggingFaceId": "zai-org/GLM-5.3-Flash", "url": "https://openrouter.ai/…" },
340
345
  "info": null,
341
- "asOf": "2026-09-23T14:02:40.271Z",
346
+ "asOf": "2026-10-02T10:46:53.452Z",
342
347
  "detailsSource": "openrouter"
343
348
  }
344
349
  ```
345
350
 
346
351
  `price.source` is `openrouter` for an `openrouter/*` entry and `vendor` for a direct one,
347
- which keeps prices honest: the refresh script never writes a vendor price. `details` comes
352
+ which keeps prices honest: the refresh script never writes a vendor price. `price.scheduled`
353
+ identifies a time-of-day list price; the Gateway row separately shows the rate in force.
354
+ `details` comes
348
355
  from the generated `model-details.json` (`npm run refresh:pricing`, see
349
356
  `docs/dev/GATEWAY.md`), `info` from the catalogue's hand-written block, and `asOf` is when
350
357
  the generated part was produced. An id the catalogue does not carry answers `404`.
@@ -652,11 +659,14 @@ One agent's spend for its panel, from `costs.db`.
652
659
 
653
660
  **Auth:** browser cookie or Bearer
654
661
 
655
- **Response:** `{ "available": true, "intervalS": 300, "today": 0.011, "week": 0.019,
662
+ **Response:** `{ "available": true, "intervalS": 300, "pricing": { "vendor": "DeepSeek", "band": "off-peak", "nextChange": 1790532000000, "source": "https://api-docs.deepseek.com/quick_start/pricing" }, "today": 0.011, "week": 0.019,
656
663
  "month": 0.022, "tokensMonth": 2409574, "topModel": { "model": "deepseek/deepseek-flash",
657
664
  "cost": 0.022 }, "unpricedMonth": 81, "collectedAt": 1790529516737, "error": null }` —
658
- UTC days (today, last 7, last 30); `collectedAt`/`error` from the agent's last read.
659
- `{ "available": false, "intervalS": 300 }` before `costs.db` exists.
665
+ UTC days (today, last 7, last 30); `pricing` is the band billed right now, the same object
666
+ the Costs page header shows (`null` when no scheduled model is on offer); `collectedAt`/`error`
667
+ from the agent's last read. Before `costs.db` exists, the response is
668
+ `{ "available": false, "intervalS": 300, "pricing": null }` (or the current pricing
669
+ object when a scheduled model is on offer).
660
670
 
661
671
  **Errors:** `400` for an invalid container name; `500` when `costs.db` cannot be read.
662
672
 
@@ -2881,8 +2891,10 @@ the update kills this process on its way in (`fuser -k <port>/tcp`) and `rev4a s
2881
2891
  brings the child back on the new binary via its `.restart-flag`, so the connection
2882
2892
  drops and the page has to wait for the server to answer again.
2883
2893
 
2884
- The child's output goes to `update.log` in the data directory — the update restarts
2885
- the process that spawned it, so that file is the only record of what happened.
2894
+ The child's output goes to `update.log` under `<REV4A_DATA_DIR>/data` (or
2895
+ `~/.config/rev4a/data` by default) — the update restarts the process that spawned
2896
+ it, so that file is the only record of what happened. The endpoint creates the
2897
+ directory if needed and reports a failure if it cannot start the updater.
2886
2898
 
2887
2899
  **Auth:** browser cookie or bearer token
2888
2900
 
@@ -1,6 +1,6 @@
1
1
  # Gateway Page
2
2
 
3
- > **Last updated:** 2026-09-27
3
+ > **Last updated:** 2026-10-02
4
4
 
5
5
  The Gateway page (`/gateway`) is the control panel for provider configuration:
6
6
  API keys and the model catalogue offered to every agent container. It does not
@@ -113,9 +113,10 @@ its own Control UI too. Rev4a computes no cost: the daemon reads OpenClaw's figu
113
113
  (cached tokens priced as if the cache gave no discount); one without `cacheWrite` to
114
114
  1.25 × the input rate — vendors that bill cache writes apart charge more than input
115
115
  (Anthropic's five-minute cache: 1.25×), and the ones that report no cache writes never
116
- use it. Either way an overestimate, never an undercount. `refresh:pricing` writes them
117
- for OpenRouter models when OpenRouter publishes them (104 and 44 of 181 entries when
118
- added); direct vendors are filled by hand from their pricing page.
116
+ use it. These fallbacks avoid treating an unknown cache rate as free, but can differ
117
+ from what a vendor bills. `refresh:pricing` writes published OpenRouter cache rates;
118
+ the file currently has cache-read rates for 107 models and cache-write rates for 44 of
119
+ 184 entries. Direct vendors are filled by hand from their pricing page.
119
120
  - **Time-of-day prices.** DeepSeek bills half its rates off-peak (peak 01:00–04:00 and
120
121
  06:00–10:00 UTC, Monday–Friday). `PRICE_SCHEDULES` in `lib/model-pricing.ts` holds
121
122
  that schedule; `priceAt()` writes the rate of the moment, and
@@ -131,6 +132,11 @@ its own Control UI too. Rev4a computes no cost: the daemon reads OpenClaw's figu
131
132
  time, so a later change of rate does not reprice a call (verified with a real call,
132
133
  priced off-peak and read again after the switch to peak). Chinese public holidays,
133
134
  which DeepSeek bills off-peak all day, are priced as peak — a small overestimate.
135
+ Where the band is visible: the Costs page header (a pill with the time of the next
136
+ change), the agent panel's COSTS section (Rates now), and the Gateway page — a
137
+ band-priced model's row carries a PEAK / OFF-PEAK chip next to its price, and that price
138
+ is the rate in force now (off-peak it shows half the list price), refreshed just after
139
+ each change while the page stays open (`nextPriceChangeMs`).
134
140
  - **Calls made before a model had a price** are priced by OpenClaw when read, at the
135
141
  rate in force then; OpenClaw reindexes its transcripts after a price change, which is
136
142
  why a read can briefly answer `cacheStatus: refreshing`.
@@ -206,6 +212,9 @@ sources, each one labelled in the modal:
206
212
  | Price | `model-pricing.json` | refreshed for `openrouter/*`, hand for direct vendors |
207
213
  | Description, specs, benchmarks | `model-details.json` | **generated**, never hand-edited |
208
214
 
215
+ For a scheduled vendor, the modal labels the static rate as a list price; the Gateway
216
+ row shows the rate in force and its peak/off-peak band.
217
+
209
218
  `npm run refresh:pricing` (script `scripts/refresh-model-pricing.mjs`) fetches the
210
219
  OpenRouter catalogue once and writes both the prices and `model-details.json`:
211
220
  description, release date (`created`), context and the provider's own limit, max output,
@@ -34,7 +34,7 @@ Modal in the agent detail panel for configuring Telegram channels. Connect with
34
34
  What an agent spent in USD. Each agent's OpenClaw prices every model call itself, with the prices Rev4a writes into it (input, output, cached input), and Rev4a reads those figures every 5 minutes. The Costs page (`/costs`) shows them for today, 7 or 30 days — by day, by agent, by model, and the most expensive sessions. The dashboard's "Spent today" card shows the same figures for today, and each agent's panel on the Agents page has a COSTS section with its own. The Costs page also compares the agents' figures with what DeepSeek and OpenRouter actually billed (their balance or usage, read every hour).
35
35
 
36
36
  ## Off-peak
37
- DeepSeek bills half price outside its peak hours (peak: 01:00–04:00 and 06:00–10:00 UTC, Monday–Friday). Rev4a writes the lower prices into the agents when off-peak starts and the full ones when it ends, so each call is priced at the rate in force when it ran. The Costs page header shows the current band.
37
+ DeepSeek bills half price outside its peak hours (peak: 01:00–04:00 and 06:00–10:00 UTC, Monday–Friday). Rev4a writes the lower prices into the agents when off-peak starts and the full ones when it ends, so each call is priced at the rate in force when it ran. The Costs page header and the agent panel's COSTS section (Rates now) show the current band.
38
38
 
39
39
  ## Cost Override
40
40
  A manual correction for a month's total cost, stored through `/api/cost-override`. There is currently no screen for it: no page in the dashboard reads or writes an override, so it can only be set by calling the API directly.
@@ -74,7 +74,7 @@ Wizard to spin up a new agent. Steps: choose a template (Prometheus, Argus, Atla
74
74
  Interactive graph showing session family trees — which agent spawned which child agent, across configurable time periods: 1d, 3d, 7d, 15d, 30d, and all. Click any node to inspect. Includes a live feed side panel.
75
75
 
76
76
  ### Gateway (`/gateway`)
77
- Connects agents to AI providers. Add and remove provider API keys, enable or disable individual models in the catalogue read from `models.config.json`, and push the resulting model list to every agent container. It does not assign models to agents: choosing which model an agent runs is done per agent, in the Model section of the agent's detail panel on the Agents page. Every model row has a **Details** button: the modal shows the model's description, price and provider, its architecture and parameter count when the weights are public, reasoning modes, capabilities, benchmarks with sources, licence and how much disk the weights take — the numbers come from the model card on Hugging Face and from curated entries that cite a source, never from estimates.
77
+ Connects agents to AI providers. Add and remove provider API keys, enable or disable individual models in the catalogue read from `models.config.json`, and push the resulting model list to every agent container. It does not assign models to agents: choosing which model an agent runs is done per agent, in the Model section of the agent's detail panel on the Agents page. A DeepSeek model row shows its current rate and a peak/off-peak chip; the rate updates after the next band change. Every model row has a **Details** button: the modal shows the model's description, list price and provider, its architecture and parameter count when the weights are public, reasoning modes, capabilities, benchmarks with sources, licence and how much disk the weights take — the numbers come from the model card on Hugging Face and from curated entries that cite a source, never from estimates.
78
78
 
79
79
  ### Containers (`/containers`)
80
80
  Full list of all Docker containers on the server, including stopped ones. Each row shows name, status, image, IP, ports, and an agent pill where applicable. Click "Terminal" to open an interactive shell into that container. A running container also shows what it consumes now — CPU share, memory and processes — with a "Charts" link to its history on the System page.
@@ -82,7 +82,7 @@ Full list of all Docker containers on the server, including stopped ones. Each r
82
82
  The page has no start, stop, or restart buttons: it is read-only apart from the terminal link. Container lifecycle is managed from the Agents page.
83
83
 
84
84
  ### Costs (`/costs`)
85
- What each agent spent, as its own OpenClaw priced it with the prices Rev4a syncs. Pick today, 7 days or 30 days (UTC days): the total, where the money went (input, output, cache), the tokens and how much input came from cache, a chart of the spend per day, the spend by agent and by model, and the most expensive sessions. Calls that could not be priced are listed by model, and agents whose figures are not current are flagged. When a DeepSeek model is on offer, the header shows whether DeepSeek is billing peak or off-peak (half price) and until when. Figures are read from the agents every 5 minutes. *Against the bill* compares them with what DeepSeek and OpenRouter actually charged (their balance or usage, read every hour); a small gap is normal, since the same key may pay for the assistant too. Each agent's own spend is also in its panel on the Agents page (COSTS).
85
+ What each agent spent, as its own OpenClaw priced it with the prices Rev4a syncs. Pick today, 7 days or 30 days (UTC days): the total, where the money went (input, output, cache), the tokens and how much input came from cache, a chart of the spend per day, the spend by agent and by model, and the most expensive sessions. Calls that could not be priced are listed by model, and agents whose figures are not current are flagged. When a DeepSeek model is on offer, the header shows whether DeepSeek is billing peak or off-peak (half price) and until when. Figures are read from the agents every 5 minutes. *Against the bill* compares them with what DeepSeek and OpenRouter actually charged (their balance or usage, read every hour); a small gap is normal, since the same key may pay for the assistant too. Each agent's own spend is also in its panel on the Agents page (COSTS), with the current DeepSeek band (Rates now).
86
86
 
87
87
  ### System (`/system`)
88
88
  The machine Rev4a runs on. Three cards — CPU (percent, cores, load average), memory (used, available, swap) and storage (each disk with its used and free space, and whether it is the system disk, where Docker keeps agents, or where Rev4a keeps its data) — then charts of CPU, memory and each disk over 1 hour, 24 hours, 7 days or 30 days (for CPU and memory the average line with the peak shaded). Hover a chart (or focus it and use the arrow keys) to read a point; each chart has a Table view. Bars turn yellow and red when they reach their thresholds. Figures refresh every 30 seconds; history is kept 30 days.
@@ -0,0 +1,9 @@
1
+ /** Types for the CommonJS Docker socket client shared with daemon.js. */
2
+
3
+ export function resolveDockerSocket(): string | null;
4
+
5
+ export function dockerRequestJson<T = unknown>(
6
+ method: string,
7
+ apiPath: string,
8
+ timeoutMs?: number,
9
+ ): Promise<T>;
@@ -0,0 +1,88 @@
1
+ /** Types for the CommonJS Docker stats parser shared with daemon.js. */
2
+
3
+ export const CONTAINER_NAME_RE: RegExp;
4
+ export const DF_TIMEOUT_MS: number;
5
+ export const MB: number;
6
+
7
+ export function cleanName(value: unknown, fallback?: string | null): string | null;
8
+ export function primaryName(names: unknown): string;
9
+ export function workingSetBytes(memory: unknown): number | null;
10
+ export function sumNetwork(networks: unknown): { rx: number | null; tx: number | null };
11
+ export function sumBlockIo(blkio: unknown): { rd: number | null; wr: number | null };
12
+
13
+ export interface ContainerCounters {
14
+ t: number;
15
+ cpuNs: number;
16
+ memBytes: number | null;
17
+ pids: number | null;
18
+ rx: number | null;
19
+ tx: number | null;
20
+ rd: number | null;
21
+ wr: number | null;
22
+ }
23
+
24
+ export function readCounters(stats: unknown, atMs: number): ContainerCounters | null;
25
+
26
+ export interface ContainerRates {
27
+ cpuCores: number | null;
28
+ rxBps: number | null;
29
+ txBps: number | null;
30
+ rdBps: number | null;
31
+ wrBps: number | null;
32
+ }
33
+
34
+ export function ratesBetween(
35
+ prev: ContainerCounters | null | undefined,
36
+ cur: ContainerCounters | null | undefined,
37
+ maxWindowS?: number,
38
+ ): ContainerRates;
39
+
40
+ export interface HostCapacity {
41
+ ncpu: number;
42
+ memTotalMb: number;
43
+ }
44
+
45
+ export interface ContainerRow {
46
+ container: string;
47
+ cpu_cores: number | null;
48
+ mem_mb: number | null;
49
+ pids: number | null;
50
+ net_rx_bps: number | null;
51
+ net_tx_bps: number | null;
52
+ blk_read_bps: number | null;
53
+ blk_write_bps: number | null;
54
+ }
55
+
56
+ export interface ContainerSource {
57
+ container: string;
58
+ agent_id: string | null;
59
+ name: string;
60
+ is_agent: 0 | 1;
61
+ volumes: string;
62
+ }
63
+
64
+ export interface ContainerSample {
65
+ ts: number;
66
+ host: HostCapacity | null;
67
+ rows: ContainerRow[];
68
+ sources: ContainerSource[];
69
+ }
70
+
71
+ export function createContainerSampler(options: {
72
+ request: (method: string, path: string, timeoutMs?: number) => Promise<unknown>;
73
+ now?: () => number;
74
+ maxWindowS?: number;
75
+ }): { sample: () => Promise<ContainerSample> };
76
+
77
+ export interface VolumeUsage {
78
+ name: string;
79
+ sizeBytes: number | null;
80
+ unused: boolean;
81
+ }
82
+
83
+ export function parseStorage(df: unknown): {
84
+ volumes: VolumeUsage[];
85
+ layers: { container: string; sizeBytes: number | null }[];
86
+ images: { sizeBytes: number | null; reclaimableBytes: number | null };
87
+ buildCache: { sizeBytes: number | null; reclaimableBytes: number | null };
88
+ } | null;
@@ -12,7 +12,7 @@ import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import { loadModelsConfig, type ModelInfo } from '@/lib/model-catalogue';
14
14
  import { providerLabel } from '@/lib/provider-labels';
15
- import { getPricing } from '@/lib/model-pricing';
15
+ import { getPricing, priceScheduleFor } from '@/lib/model-pricing';
16
16
 
17
17
  /** What `scripts/refresh-model-pricing.mjs` writes per id. All optional. */
18
18
  export interface GeneratedDetails {
@@ -48,7 +48,7 @@ export interface ModelDetailsView {
48
48
  modality?: string;
49
49
  enabled: boolean;
50
50
  deprecated: boolean;
51
- price: { input: number; output: number; source: 'openrouter' | 'vendor' } | null;
51
+ price: { input: number; output: number; source: 'openrouter' | 'vendor'; scheduled: boolean } | null;
52
52
  details: GeneratedDetails | null;
53
53
  info: ModelInfo | null;
54
54
  /** When the generated part was produced. */
@@ -115,7 +115,7 @@ export function getModelDetails(id: string): ModelDetailsView | null {
115
115
  modality: entry.modality,
116
116
  enabled: entry.enabled,
117
117
  deprecated: entry.deprecated === true,
118
- price: pricing ? { ...pricing, source: id.startsWith('openrouter/') ? 'openrouter' : 'vendor' } : null,
118
+ price: pricing ? { ...pricing, source: id.startsWith('openrouter/') ? 'openrouter' : 'vendor', scheduled: !!priceScheduleFor(id) } : null,
119
119
  details,
120
120
  info,
121
121
  asOf: file.generatedAt ?? null,
@@ -108,7 +108,9 @@ export function formatPricing(p: ModelPricing | null | undefined): string {
108
108
 
109
109
  /** The time-of-day schedule a model is billed on, or null for a flat price. */
110
110
  export function priceScheduleFor(modelId: string): PriceSchedule | null {
111
- return PRICE_SCHEDULES[modelId.split('/')[0]] ?? null;
111
+ // Own keys only: an id segment like "constructor" must not find Object.prototype.
112
+ const vendor = modelId.split('/')[0];
113
+ return Object.hasOwn(PRICE_SCHEDULES, vendor) ? PRICE_SCHEDULES[vendor] : null;
112
114
  }
113
115
 
114
116
  function isPeak(schedule: PriceSchedule, at: Date): boolean {
@@ -145,9 +147,9 @@ const CACHE_WRITE_FALLBACK = 1.25;
145
147
  *
146
148
  * Null when there is nothing to write: no entry, or a dynamic router price (-1), whose
147
149
  * calls OpenClaw then counts as unpriced. A free model (all zero) gets FREE_RATE. A cache
148
- * rate the file does not carry falls back to a rate that does not undercount: the input
149
- * rate for a cache read (as if the cache gave no discount), CACHE_WRITE_FALLBACK × input
150
- * for a cache write.
150
+ * rate the file does not carry falls back to the input rate for a cache read (as if the
151
+ * cache gave no discount), or CACHE_WRITE_FALLBACK × input for a cache write. These
152
+ * estimates avoid treating an unknown rate as free; they are not vendor quotes.
151
153
  */
152
154
  export function priceAt(modelId: string, at: Date = new Date()): ModelCost | null {
153
155
  const p = getPricing(modelId);