@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
package/docs/DESIGN-SYSTEM.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Source of truth: `app/design-system.ts` + CSS tokens in `app/globals.css`.
|
|
4
4
|
|
|
5
|
-
> **Last updated:** 2026-09-
|
|
5
|
+
> **Last updated:** 2026-09-27
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -82,7 +82,7 @@ then use it — a one-off copy of markup is the thing this section exists to pre
|
|
|
82
82
|
5. **CSS vars over hardcoded colors.** Never inline `#22c55e`, `#ef4444`, `#888` — use the CSS token.
|
|
83
83
|
6. **Loading feedback.** Use `loading` prop on `Button` (shows inline spinner). Do not render separate loader elements.
|
|
84
84
|
7. **Minimal animation.** Transitions on hover/active (0.1s–0.12s), spin on loading spinner. No bounce, shake, or decorative animations.
|
|
85
|
-
8. **Data display: violet for the data, status colours for levels.** A chart series is `var(--violet)` (2px line, 10% wash), grid and axis text are `var(--border)` / `var(--text-dim)`, never the series colour. A `Meter` fill is violet when fine and `var(--yellow)` / `var(--red)` at its warning / critical level, with the level also written out — status never rests on colour alone. Focus on a chart is a 1px `outline` in `var(--violet-dim)`, not a shadow. See `TimeSeriesChart` and `Meter` in `docs/FRONTEND-ARCHITECTURE.md`.
|
|
85
|
+
8. **Data display: violet for the data, status colours for levels.** A chart series is `var(--violet)` (2px line, 10% wash), grid and axis text are `var(--border)` / `var(--text-dim)`, never the series colour. A `Meter` fill is violet when fine and `var(--yellow)` / `var(--red)` at its warning / critical level, with the level also written out — status never rests on colour alone. Focus on a chart is a 1px `outline` in `var(--violet-dim)`, not a shadow. A figure that is only a share of a whole (the Costs page's agents and models) uses the same `Meter`, always at its `ok` level: it is a proportion, not a warning. See `TimeSeriesChart` and `Meter` in `docs/FRONTEND-ARCHITECTURE.md`.
|
|
86
86
|
|
|
87
87
|
### React hook
|
|
88
88
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a Frontend Architecture
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-09-
|
|
3
|
+
> **Last updated:** 2026-09-27
|
|
4
4
|
|
|
5
5
|
## Layering
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
39
39
|
| `Metric` | `Metric.tsx` | Metric card with title, value, subtitle, tone, and a `size` (`md` default, `sm` for a grid inside a dialog: smaller value, tighter spacing). |
|
|
40
40
|
| `StatusCard` | `StatusCard.tsx` | Health/status report card. |
|
|
41
41
|
| `Meter` / `meterLevel` | `Meter.tsx` | Horizontal 0-100 % fill whose colour carries the level (`ok` accent, `warning` yellow, `critical` red); the level is also written out beside it, never colour alone. `meterLevel(percent, warning, critical)` picks the level. ARIA `role="meter"`. |
|
|
42
|
-
| `TimeSeriesChart` | `TimeSeriesChart.tsx` | One series over time in plain SVG measured to its container (text never scaled): a 2px accent line for the value, a 10 % wash up to the bucket's peak, a hairline grid at 0 / 50 / 100 %, the latest value labelled at the line's end. Points further apart than 1.5 buckets are not joined (a gap means no samples). Hover or keyboard focus (← → Esc) shows a crosshair snapped to the nearest point with value and peak; a **Table** disclosure holds the same data. `stale` dims the previous render while a new range loads. Used by the System
|
|
42
|
+
| `TimeSeriesChart` | `TimeSeriesChart.tsx` | One series over time in plain SVG measured to its container (text never scaled): a 2px accent line for the value, a 10 % wash up to the bucket's peak, a hairline grid at 0 / 50 / 100 %, the latest value labelled at the line's end. Points further apart than 1.5 buckets are not joined (a gap means no samples). Hover or keyboard focus (← → Esc) shows a crosshair snapped to the nearest point with value and peak; a **Table** disclosure holds the same data. `stale` dims the previous render while a new range loads. `yMax` / `unit` scale and label a percentage by default; `formatValue` replaces the label format everywhere (axis, end label, tooltip, table) — the Costs page passes dollars. The left and right margins grow with the longest axis label and the end label, so a longer format is never cut. Used by the System and Costs pages. |
|
|
43
43
|
| `Surface` | `Surface.tsx` | Shared panel/card surface, variant prop. |
|
|
44
44
|
| `Page` / `PageHeader` | `Page.tsx` | Full-page layout shell. |
|
|
45
45
|
| `Icons` | `Icons.tsx` | SVG icons (`EyeIcon`, `EyeOffIcon`), 16/20px shared. |
|
|
@@ -48,6 +48,8 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
48
48
|
| `BackupSection` | inline in `app/agents/PageClient.tsx` | BACKUP section of the agent detail panel, on the cold backup and the restore. **Backup Now** starts `POST /api/agents/[id]/cold-backup`; while the job runs a banner shows the file and its live percent with **Cancel** (`DELETE /cold-backup`). **Restore** (after a confirm) starts `POST /restore` and a banner shows `Restoring <file>…` (no percent: the extract is a single `tar xzf`, and there is no Cancel). Both sections poll their `GET` every 2 s while running, and on mount pick up a job that is already running — a backup lives in a Docker helper, a restore in `agent_restores`, so reloading the page or navigating away never loses them nor allows a second one (the server answers 409 anyway). Delete per row; all actions disabled while one runs; keyed busy state `{ kind, file }` so only the row in action shows the spinner. On the agent list, an activity Badge (fed by `/api/agents/activity-summary`, polled at 2 s only while something runs, otherwise riding the 15 s list poll) reads `BACKUP nn%`, `RESTORING`, `RECREATING`, `EDITING` or `UPDATING`. |
|
|
49
49
|
| `RecreateSection` | inline in `app/agents/PageClient.tsx` | RECREATE section of the agent detail panel. **Recreate Container** starts `POST /api/agents/[id]/recreate` (202) after a confirm; a banner then shows the phase — *Backing up … nn%* while the cold backup runs, *Recreating container…* while the container is rebuilt and the gateway starts. The section polls `GET /recreate` every 2 s, and on mount picks up a recreate that is already running, so a reload or navigation never loses it; it refetches the agent once the job reports `done`. |
|
|
50
50
|
| System page | `app/system/PageClient.tsx` | `/system`: CPU, memory and storage of the host, from `GET /api/metrics`. Three cards (CPU with cores and load; memory with available and swap; one `Meter` per filesystem with its roles) — thresholds 85/95 % for CPU and memory, 80/90 % for storage — then a range switch (`Tabs`: 1h · 24h · 7d · 30d) scoping the charts below it: CPU and memory average with peak, one chart per filesystem. Polls every 30 s with an `AbortController` ref (a range change aborts the previous fetch). Times in the configured Rev4a timezone (`useRev4aTimezone`). Before the first answer the cards and charts are skeletons shaped like them (`app/system/SystemSkeleton.tsx`, also the route's `loading.tsx`), so nothing jumps; the hostname line is cut with an ellipsis and never widens the page. Says *No samples yet* before the daemon's first sample and *Not collecting* when the latest sample is older than three intervals. Two columns of charts from lg (992px) up, one below. |
|
|
51
|
+
| Costs page | `app/costs/PageClient.tsx` | `/costs`: what each agent spent, as its own OpenClaw priced it, from `GET /api/costs/usage`. A range switch (`Tabs`: Today · 7 days · 30 days, UTC days) scopes everything below it: three cards (spent with tokens and days; the cost split into input / output / cache read / cache write; the tokens with the share of input served from cache), the daily spend (`TimeSeriesChart` with dollars, not on Today), then *By agent* and *By model* side by side — each row a name and figure over a share `Meter`, the pattern of the System storage card — *Against the bill* (per vendor, what it billed, from balance or usage readings, next to what the agents priced over the same readings, top-ups apart), and the most expensive sessions. Warnings above: *No figures yet* before the daemon's first pass, *Not current* for agents still being read whose last read failed or is older than three intervals (a stopped or deleted agent keeps its figures unflagged), and *Calls without a price* by model (a model with no price is also marked in *By model*, so a $0.00 is never read as free). The header shows the DeepSeek band now (peak / off-peak, until when, UTC) when a DeepSeek model is on offer. Polls every 60 s with an `AbortController` ref. Skeletons shaped like the content (`app/costs/CostsSkeleton.tsx`, also the route's `loading.tsx`). Costs under a cent keep four decimals. |
|
|
52
|
+
| Agent costs | `app/agents/CostSection.tsx` | The COSTS section of the agent panel, after MODEL: today (UTC), last 7 and 30 days with tokens, the top model, calls without a price, when the agent was last read (a note when not recently: a stopped agent keeps its last figures), and a link to `/costs`. From `GET /api/costs/agent`, every 60 s with an `AbortController` ref. |
|
|
51
53
|
| Machine card | `app/components/SystemCockpit.tsx` | On the dashboard, next to Active sessions: CPU, RAM and the fullest disk now, each figure coloured only when past its threshold (same thresholds as the System page), the issues named in words, and a link to `/system`. It says *No samples yet* before the first sample, *Not collecting* when the newest is stale, and *Metrics unavailable* when a refresh fails (keeping the last figures). A `Surface`, not a `Metric`: a danger `Metric` paints its whole value red. |
|
|
52
54
|
| Container terminal | `app/containers/terminal/[id]/TerminalClient.tsx` | Full-screen shell into a container: xterm.js 6 (`@xterm/xterm` + `addon-fit` refitted by a `ResizeObserver`, `addon-web-links`), loaded client-side only, palette from the design tokens. Keystrokes are sent raw (`onData`, `onBinary`), the size on connect and whenever the columns or rows change. A bar with `ui-kicker`, the container name, a status `Badge` (Connecting / Connected / Reconnecting / Closed) and `Button`s **Reconnect** / **Close**; styles are the `terminal-page` classes. Session id per container in `sessionStorage` (a UUID from `getRandomValues`: `randomUUID` is missing over plain HTTP), so a reload resumes the shell; after a drop it reconnects with `resume=1` and is told when the shell is gone; retries with backoff (six failed opens, about 40 s, then a message) and stops on the server's final close codes with the reason shown. **Close** while reconnecting still ends the waiting shell. Protocol and sessions: `docs/CONTAINER-TERMINAL.md`. |
|
|
53
55
|
| `ImageDownloadBanner` | `app/agents/ImageDownloadBanner.tsx` | Agent image banner on the Agents page. Polls `/api/agents/image-status` every 2 s; offers **Download Image** when no supported version is downloaded, **Download <version>** when the registry publishes a newer one, and while a download runs shows the percent of layers finished and the latest line of docker's output (the bar is indeterminate until a percent can be computed). **Cancel** (`DELETE /api/agents/download-image`, own loading state) appears once `image-status` reports the download running; before that the button reads *Starting…* and is disabled. Every outcome — ready and cancelled for 3 s, a failure until the next action — comes from the server's `lastResult`, so one that ended while the page was closed or reloading is still reported if it is less than 30 s old (aged with `serverTime`). Each outcome is announced once per page load, although the Agents page mounts the banner in three places (mobile list, mobile detail, desktop). Downloading changes no agent. |
|
package/docs/REV4A.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a — VPS Dashboard
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-09-
|
|
3
|
+
> **Last updated:** 2026-09-27
|
|
4
4
|
|
|
5
5
|
A Next.js 16 dashboard for monitoring and managing the OpenClaw ecosystem.
|
|
6
6
|
|
|
@@ -68,6 +68,7 @@ Every agent is created with:
|
|
|
68
68
|
- Control UI at `http://<host>:<port>`, opened from the Agents page through `GET /api/agents/[id]/open-control-ui`
|
|
69
69
|
- The provider gateway at `http://host.docker.internal:3740/api/provider/v1`
|
|
70
70
|
- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback: true`, so the Control UI opens on whatever host the agent is reached on, and pages from other origins are refused
|
|
71
|
+
- `--init`: Docker's tini runs as PID 1, with OpenClaw as its child, and collects orphaned processes the moment they exit. Without it `openclaw-gateway` is PID 1 and never collects them: every process a tool leaves behind (Chrome, builds, scripts) stays a zombie and counts against the container's pids limit (9 365 on the VPS) until the container cannot start anything. Measured before the fix: 1 542 zombies in one agent after four days of browsing and builds. tini forwards `docker stop`'s SIGTERM, so OpenClaw still shuts down cleanly. Set at create and in `recreateAgentContainer` (recreate, update, edit); an existing agent gets it the next time it is recreated.
|
|
71
72
|
|
|
72
73
|
The port is auto-assigned starting from 3000 (or user-specified).
|
|
73
74
|
|
|
@@ -98,9 +99,10 @@ When environment variables are saved via the Config page (Save & Restart):
|
|
|
98
99
|
|
|
99
100
|
| Route | Description |
|
|
100
101
|
|---|---|
|
|
101
|
-
| `/` | System overview — health, sessions, the machine's CPU / RAM / disk (links to `/system`),
|
|
102
|
+
| `/` | System overview — health, sessions, the machine's CPU / RAM / disk (links to `/system`), what the agents spent today (as on `/costs`), live feed |
|
|
102
103
|
| `/agents` | Running agents (Docker containers with `AGENT_ID`), gateway token management, agent creation wizard, channel manager (Telegram pairing) |
|
|
103
104
|
| `/containers` | All Docker containers on the host |
|
|
105
|
+
| `/costs` | What each agent spent, as its own OpenClaw priced it with the rates Rev4a syncs: today / 7 / 30 UTC days, by day, by agent, by model, the most expensive sessions, the calls that could not be priced, the DeepSeek band now, and a check against what DeepSeek and OpenRouter actually billed (read by the daemon every 5 min into `costs.db`; each agent's own figures are also in its panel, COSTS) |
|
|
104
106
|
| `/system` | The host machine: CPU, memory and swap, storage per filesystem — current values and 1h / 24h / 7d / 30d history (from `metrics.db`, sampled by the daemon every 30 s) |
|
|
105
107
|
| `/wizard` | First-run setup wizard (Welcome → Providers → Agent → Ready) |
|
|
106
108
|
| `/workspace` | File explorer with tree view + editor — VPS host or container workspaces |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a API Reference
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-09-
|
|
3
|
+
> **Last updated:** 2026-09-27
|
|
4
4
|
|
|
5
5
|
All routes are under `/api/`. Authentication is required on every endpoint
|
|
6
6
|
unless otherwise noted.
|
|
@@ -469,12 +469,12 @@ Returns all sessions (up to 2000), newest first, joined with lineage labels.
|
|
|
469
469
|
"model": "openai-codex/gpt-5.4",
|
|
470
470
|
"tokens_in": 12500,
|
|
471
471
|
"tokens_out": 3200,
|
|
472
|
-
"cost_usd": 0
|
|
472
|
+
"cost_usd": 0,
|
|
473
473
|
"status": "idle",
|
|
474
474
|
"task_preview": "Run hygiene audit...",
|
|
475
|
-
"started_at":
|
|
475
|
+
"started_at": 1749200000000,
|
|
476
476
|
"ended_at": null,
|
|
477
|
-
"updated_at":
|
|
477
|
+
"updated_at": 1749201000000,
|
|
478
478
|
"trello_card_url": null,
|
|
479
479
|
"lineage_label": null,
|
|
480
480
|
"lineage_agent_name": null
|
|
@@ -517,8 +517,76 @@ One session with its events and any child sessions.
|
|
|
517
517
|
|
|
518
518
|
## Stats & Costs
|
|
519
519
|
|
|
520
|
+
Agent costs come from `costs.db`: what each agent's own OpenClaw priced, copied by the
|
|
521
|
+
daemon (the queries are in `lib/agent-costs.ts` (spend) and `lib/cost-reconciliation.ts` (`vendor_balance`), on a read-only handle from `lib/costs-db.ts`). `GET /api/costs/usage` feeds the Costs
|
|
522
|
+
page, `GET /api/costs` the dashboard, `GET /api/costs/agent` the agent panel. `GET
|
|
523
|
+
/api/stats` and `GET /api/stats-since` still read the `sessions` table of `events.db`,
|
|
524
|
+
whose `cost_usd` is no longer estimated (always 0).
|
|
525
|
+
|
|
526
|
+
### `GET /api/costs/usage?range=today|7d|30d`
|
|
527
|
+
What the agents spent in a range of UTC days, today included (default `7d`), read from
|
|
528
|
+
`costs.db` (written by the daemon every 5 minutes, see `docs/dev/DATABASE.md`). Rev4a
|
|
529
|
+
computes no cost: every figure is the one OpenClaw recorded with the prices Rev4a syncs.
|
|
530
|
+
|
|
531
|
+
**Auth:** browser cookie or Bearer
|
|
532
|
+
|
|
533
|
+
**Response:**
|
|
534
|
+
```json
|
|
535
|
+
{
|
|
536
|
+
"available": true,
|
|
537
|
+
"range": "7d",
|
|
538
|
+
"startDate": "2026-09-21",
|
|
539
|
+
"endDate": "2026-09-27",
|
|
540
|
+
"intervalS": 300,
|
|
541
|
+
"pricing": { "vendor": "DeepSeek", "band": "off-peak", "nextChange": 1790557200000, "source": "https://api-docs.deepseek.com/quick_start/pricing" },
|
|
542
|
+
"totals": { "cost": 0.0086, "tokens": 114053, "input": 47436, "output": 2233, "cacheRead": 64384, "cacheWrite": 0,
|
|
543
|
+
"inputCost": 0.0071, "outputCost": 0.0013, "cacheReadCost": 0.0002, "cacheWriteCost": 0, "missing": 0 },
|
|
544
|
+
"daily": [ { "date": "2026-09-21", "cost": 0.0071, "tokens": 96806, "missing": 0 } ],
|
|
545
|
+
"byAgent": [ { "container": "agent_9253eee3", "agentId": "agent_9253eee3", "name": "test", "cost": 0.0077, "tokens": 96806,
|
|
546
|
+
"missing": 0, "collectedAt": 1790526404843, "attemptedAt": 1790526404843, "error": null } ],
|
|
547
|
+
"byModel": [ { "provider": "rev4a", "model": "deepseek/deepseek-flash", "cost": 0.0086, "tokens": 114053, "calls": 13 } ],
|
|
548
|
+
"sessions": [ { "container": "agent_9253eee3", "agentName": "test", "sessionId": "…", "sessionKey": "agent:main:…", "label": "…",
|
|
549
|
+
"agentId": "main", "model": "deepseek/deepseek-flash", "cost": 0.004, "tokens": 18681, "missing": 0, "lastActivity": 1790322536609 } ],
|
|
550
|
+
"unpriced": [ { "model": "rev4a/deepseek/deepseek-v4-flash", "calls": 84, "agents": ["test", "test 2"] } ],
|
|
551
|
+
"reconciliation": [ { "vendor": "deepseek", "currency": "USD", "from": 1790529518869, "to": 1790536317000,
|
|
552
|
+
"billed": 0.02, "priced": 0.0171, "topUps": 0, "readings": 3, "comparable": true } ]
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
- `daily` has one entry per day of the range, zeros included.
|
|
557
|
+
- `missing` counts calls OpenClaw could not price (no price for their model): they are in
|
|
558
|
+
the tokens, not in the cost. `unpriced` lists them by model over each agent's last 30
|
|
559
|
+
days.
|
|
560
|
+
- `byAgent` holds every agent read, running or not, with use in the range, an error, or
|
|
561
|
+
a read in the last 15 minutes; `error` is why its last read stored nothing (OpenClaw's
|
|
562
|
+
own reason, e.g. "Gateway not reachable…"). An agent still being read (`attemptedAt`
|
|
563
|
+
within 3 × `intervalS`) with an `error` or a `collectedAt` older than that is not
|
|
564
|
+
current; a stopped or deleted agent is no longer read and keeps its last figures.
|
|
565
|
+
- `sessions`: the 20 most expensive sessions active in the range; a session's figures are
|
|
566
|
+
its total over the last 30 days.
|
|
567
|
+
- `pricing`: the band DeepSeek bills at now and when it changes, when a DeepSeek model is
|
|
568
|
+
on offer; `null` otherwise.
|
|
569
|
+
- `reconciliation`: per vendor with a configured key (DeepSeek, OpenRouter), what it
|
|
570
|
+
billed next to what the agents priced over the same hourly readings — `{ vendor,
|
|
571
|
+
currency, from, to, billed, priced, topUps, readings, comparable }`. `billed` is the
|
|
572
|
+
balance drop (DeepSeek; rises are `topUps`, not spend) or the usage growth (OpenRouter);
|
|
573
|
+
`priced` the growth of the agents' total for that vendor's models. `billed`/`priced`
|
|
574
|
+
are `null` until two readings exist; `comparable` is false for a non-USD balance.
|
|
575
|
+
`vendor` is the lowercase key (`deepseek`, `openrouter`); `pricing.vendor` is a display
|
|
576
|
+
name. Known gaps, shown rather than corrected: the same key may also pay for the
|
|
577
|
+
assistant or clients outside Rev4a; a top-up and spend in the same hour net out; an
|
|
578
|
+
agent missed by a pass joins the agents' total one pass later; calls never priced are
|
|
579
|
+
priced by OpenClaw when read, so the agents' total can grow for calls made earlier.
|
|
580
|
+
- `available: false` (zeros, empty lists) until the daemon has created `costs.db`.
|
|
581
|
+
|
|
582
|
+
**Errors:** `400` for another `range`; `500` when `costs.db` cannot be read.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
520
586
|
### `GET /api/stats`
|
|
521
|
-
Current-month aggregate stats (tokens + cost) grouped by model
|
|
587
|
+
Current-month aggregate stats (tokens + cost) grouped by model, from the `sessions` table
|
|
588
|
+
(`started_at` is in milliseconds). The costs are the `sessions.cost_usd` column, which is no
|
|
589
|
+
longer estimated: 0, except rows written before 2026-09-27. Agent spend is `GET /api/costs/usage`.
|
|
522
590
|
|
|
523
591
|
**Auth:** Bearer or browser cookie
|
|
524
592
|
|
|
@@ -526,13 +594,13 @@ Current-month aggregate stats (tokens + cost) grouped by model.
|
|
|
526
594
|
```json
|
|
527
595
|
{
|
|
528
596
|
"total": {
|
|
529
|
-
"total":
|
|
597
|
+
"total": 0,
|
|
530
598
|
"total_in": 850000,
|
|
531
599
|
"total_out": 210000,
|
|
532
600
|
"sessions": 47
|
|
533
601
|
},
|
|
534
602
|
"byModel": [
|
|
535
|
-
{ "model": "
|
|
603
|
+
{ "model": "rev4a/deepseek/deepseek-flash", "cost": 0, "tokens_in": 600000, "tokens_out": 150000, "sessions": 30 }
|
|
536
604
|
]
|
|
537
605
|
}
|
|
538
606
|
```
|
|
@@ -540,7 +608,8 @@ Current-month aggregate stats (tokens + cost) grouped by model.
|
|
|
540
608
|
---
|
|
541
609
|
|
|
542
610
|
### `GET /api/stats-since?ts=<unix_ms>`
|
|
543
|
-
|
|
611
|
+
Sum of `sessions.cost_usd` since a given timestamp — 0 for sessions polled since 2026-09-27
|
|
612
|
+
(see `GET /api/stats`).
|
|
544
613
|
|
|
545
614
|
**Auth:** Bearer or browser cookie
|
|
546
615
|
|
|
@@ -548,28 +617,49 @@ Total cost (USD) since a given timestamp.
|
|
|
548
617
|
|
|
549
618
|
**Response:**
|
|
550
619
|
```json
|
|
551
|
-
{ "total": 0
|
|
620
|
+
{ "total": 0 }
|
|
552
621
|
```
|
|
553
622
|
|
|
554
623
|
---
|
|
555
624
|
|
|
556
625
|
### `GET /api/costs`
|
|
557
|
-
|
|
626
|
+
The dashboard's figures: today's spend (UTC day), all-time spend, today's calls without a
|
|
627
|
+
price, and today's spend by model — from `costs.db`. The SSE stream (`GET /api/stream`)
|
|
628
|
+
sends the same `today`.
|
|
558
629
|
|
|
559
|
-
**Auth:** browser cookie
|
|
630
|
+
**Auth:** browser cookie or Bearer
|
|
560
631
|
|
|
561
632
|
**Response:**
|
|
562
633
|
```json
|
|
563
634
|
{
|
|
564
|
-
"today": 0.
|
|
565
|
-
"
|
|
566
|
-
"
|
|
567
|
-
"
|
|
568
|
-
"
|
|
569
|
-
"byModel": [ ... ]
|
|
635
|
+
"today": 0.0112,
|
|
636
|
+
"allTime": 0.0559,
|
|
637
|
+
"allTimeSource": "agents_openclaw",
|
|
638
|
+
"missingToday": 0,
|
|
639
|
+
"byModel": [ { "model": "deepseek/deepseek-flash", "cost_usd": 0.0112, "tokens": 244157, "calls": 7 } ]
|
|
570
640
|
}
|
|
571
641
|
```
|
|
572
642
|
|
|
643
|
+
`byModel` lists the models that cost something today; the free and unpriced ones are on the
|
|
644
|
+
Costs page.
|
|
645
|
+
|
|
646
|
+
**Errors:** `500` when `costs.db` cannot be read.
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
### `GET /api/costs/agent?container=<name>`
|
|
651
|
+
One agent's spend for its panel, from `costs.db`.
|
|
652
|
+
|
|
653
|
+
**Auth:** browser cookie or Bearer
|
|
654
|
+
|
|
655
|
+
**Response:** `{ "available": true, "intervalS": 300, "today": 0.011, "week": 0.019,
|
|
656
|
+
"month": 0.022, "tokensMonth": 2409574, "topModel": { "model": "deepseek/deepseek-flash",
|
|
657
|
+
"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.
|
|
660
|
+
|
|
661
|
+
**Errors:** `400` for an invalid container name; `500` when `costs.db` cannot be read.
|
|
662
|
+
|
|
573
663
|
---
|
|
574
664
|
|
|
575
665
|
### `GET /api/cost-override?month=YYYY-MM`
|
|
@@ -619,13 +709,17 @@ Paginated event log.
|
|
|
619
709
|
---
|
|
620
710
|
|
|
621
711
|
### `GET /api/stream`
|
|
622
|
-
Server-Sent Events stream.
|
|
712
|
+
Server-Sent Events stream. Every 5 s it sends one message per kind, each `{ type, data }`:
|
|
713
|
+
`events` (new rows of the `events` table, only when there are any), `sessions` (the 100 most
|
|
714
|
+
recent sessions with their lineage label) and `costs` (`{ "today": … }`, what the agents spent
|
|
715
|
+
today, UTC day — the same figure as `GET /api/costs`).
|
|
623
716
|
|
|
624
|
-
**Auth:** browser cookie
|
|
717
|
+
**Auth:** browser cookie or Bearer
|
|
625
718
|
|
|
626
719
|
**Event format:**
|
|
627
720
|
```
|
|
628
|
-
data: {"
|
|
721
|
+
data: {"type":"sessions","data":[...]}
|
|
722
|
+
data: {"type":"costs","data":{"today":0.0112}}
|
|
629
723
|
```
|
|
630
724
|
|
|
631
725
|
Use `EventSource` in the browser or `curl -N` for testing.
|
|
@@ -1973,14 +2067,16 @@ say so; it does not fail the others.
|
|
|
1973
2067
|
{ "id": "runtime.sessions", "label": "Runtime sessions", "health": "ok", "value": 1, "details": "last session activity 3m ago", "source": "runtime" }
|
|
1974
2068
|
],
|
|
1975
2069
|
"recommendations": [
|
|
1976
|
-
{ "id": "cost.review-usage-based", "severity": "warning", "source": "cost", "title": "High
|
|
2070
|
+
{ "id": "cost.review-usage-based", "severity": "warning", "source": "cost", "title": "High spend today", "details": "…", "actionHref": "/costs", "dismissible": true, "createdAt": "2026-09-26T10:00:00.000Z" }
|
|
1977
2071
|
],
|
|
1978
2072
|
"generatedAt": "2026-09-26T10:00:00.000Z"
|
|
1979
2073
|
}
|
|
1980
2074
|
```
|
|
1981
2075
|
|
|
1982
2076
|
Checks: `runtime.sessions`, `runtime.heartbeat`, `runtime.errors24h`,
|
|
1983
|
-
`cost.usageBasedToday
|
|
2077
|
+
`cost.usageBasedToday` ("Spent today": the agents' spend today, UTC day, from `costs.db`;
|
|
2078
|
+
a warning above $10 or with calls without a price, each with its recommendation,
|
|
2079
|
+
`cost.review-usage-based` / `cost.unpriced-calls`), `cron.jobs`. `health` (overall and per check) is `"ok"` /
|
|
1984
2080
|
`"warning"` / `"error"`; a recommendation's `severity` is `"info"` / `"warning"` /
|
|
1985
2081
|
`"critical"`. `generatedAt` and `createdAt` are ISO strings.
|
|
1986
2082
|
|
package/docs/dev/DATABASE.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Rev4a — Database Reference
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-09-
|
|
3
|
+
> **Last updated:** 2026-09-27
|
|
4
4
|
|
|
5
|
-
Rev4a keeps
|
|
5
|
+
Rev4a keeps four SQLite files in WAL mode, by default all in the data directory's `data/`:
|
|
6
6
|
`events.db` (sessions and events, below), `metrics.db` (machine metrics, see
|
|
7
|
-
[Metrics Database](#metrics-database-metricsdb))
|
|
7
|
+
[Metrics Database](#metrics-database-metricsdb)), `costs.db` (what the agents spent, see
|
|
8
|
+
[Costs Database](#costs-database-costsdb)) and `credentials.db` (see
|
|
8
9
|
[Credentials Database](#credentials-database-credentialsdb)).
|
|
9
10
|
|
|
10
11
|
## Location
|
|
@@ -23,7 +24,7 @@ PRAGMA synchronous = NORMAL;
|
|
|
23
24
|
|
|
24
25
|
- The daemon writes; the Next.js API routes open read-only connections
|
|
25
26
|
- `events.db`: `PRAGMA wal_checkpoint(PASSIVE)` after each daemon poll cycle, `FULL` every 10
|
|
26
|
-
- `metrics.db`: SQLite's automatic checkpoint (every 1000 pages) — no explicit one
|
|
27
|
+
- `metrics.db`, `costs.db`: SQLite's automatic checkpoint (every 1000 pages) — no explicit one
|
|
27
28
|
- WAL files (`*.db-shm`, `*.db-wal`) — do not delete while the daemon is running
|
|
28
29
|
|
|
29
30
|
## Tables
|
|
@@ -38,11 +39,11 @@ CREATE TABLE sessions (
|
|
|
38
39
|
model TEXT,
|
|
39
40
|
tokens_in INTEGER DEFAULT 0,
|
|
40
41
|
tokens_out INTEGER DEFAULT 0,
|
|
41
|
-
cost_usd REAL DEFAULT 0,
|
|
42
|
+
cost_usd REAL DEFAULT 0, -- always 0 since 2026-09-27: agent costs are in costs.db
|
|
42
43
|
status TEXT DEFAULT 'idle',
|
|
43
44
|
task_preview TEXT,
|
|
44
|
-
started_at INTEGER, -- Unix
|
|
45
|
-
ended_at INTEGER, -- Unix
|
|
45
|
+
started_at INTEGER, -- Unix ms
|
|
46
|
+
ended_at INTEGER, -- Unix ms, null if active
|
|
46
47
|
updated_at INTEGER, -- Unix ms, updated only on real changes
|
|
47
48
|
trello_card_url TEXT
|
|
48
49
|
);
|
|
@@ -59,13 +60,19 @@ CREATE TABLE events (
|
|
|
59
60
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
61
|
ts INTEGER NOT NULL, -- Unix ms
|
|
61
62
|
session_id TEXT NOT NULL,
|
|
62
|
-
type TEXT NOT NULL, --
|
|
63
|
+
type TEXT NOT NULL, -- see below
|
|
63
64
|
data TEXT -- JSON payload
|
|
64
65
|
);
|
|
65
66
|
|
|
66
67
|
CREATE INDEX idx_events_session ON events(session_id, ts);
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
Types written today: `spawn`, `complete`, `fail`, `spawn_timeout` (the daemon's session poll),
|
|
71
|
+
`system_anomaly` (the daemon's machine metrics, session `system`), `agent_created` and
|
|
72
|
+
`agent_deleted` (the create and delete routes, session `system:agents`). **No retention:**
|
|
73
|
+
rows are never pruned — on a development machine with a nearly full disk, `system_anomaly`
|
|
74
|
+
reached ~4 000 rows in a month.
|
|
75
|
+
|
|
69
76
|
### `lineage` — explicit parent→child declarations
|
|
70
77
|
|
|
71
78
|
```sql
|
|
@@ -90,7 +97,7 @@ CREATE TABLE cost_override (
|
|
|
90
97
|
);
|
|
91
98
|
```
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
Nothing reads `cost_override` except `/api/cost-override` itself: no page shows an override.
|
|
94
101
|
|
|
95
102
|
The daemon also writes a `system_anomaly` event (session `system`) when the machine
|
|
96
103
|
crosses a threshold — see [Metrics Database](#metrics-database-metricsdb). No feature
|
|
@@ -195,18 +202,21 @@ container started again. Rows are never pruned.
|
|
|
195
202
|
## Useful Queries
|
|
196
203
|
|
|
197
204
|
```sql
|
|
198
|
-
--
|
|
199
|
-
SELECT SUM(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
-- costs.db: what the agents spent this month (UTC days), by agent
|
|
206
|
+
SELECT s.name, SUM(d.cost) AS spent, SUM(d.missing) AS unpriced
|
|
207
|
+
FROM agent_cost_daily d LEFT JOIN agent_cost_sources s USING (container)
|
|
208
|
+
WHERE d.date >= strftime('%Y-%m-01', 'now')
|
|
209
|
+
GROUP BY d.container ORDER BY spent DESC;
|
|
210
|
+
|
|
211
|
+
-- costs.db: spend by model, last 30 days
|
|
212
|
+
SELECT model, SUM(cost) AS spent, SUM(calls) AS calls
|
|
213
|
+
FROM agent_cost_model_daily WHERE date >= date('now', '-29 days')
|
|
214
|
+
GROUP BY provider, model ORDER BY spent DESC;
|
|
215
|
+
|
|
216
|
+
-- events.db: active sessions right now
|
|
217
|
+
SELECT session_id, label, model, status
|
|
204
218
|
FROM sessions WHERE status = 'working';
|
|
205
219
|
|
|
206
|
-
-- Cost by model (all time)
|
|
207
|
-
SELECT model, SUM(cost_usd) AS total, COUNT(*) AS sessions
|
|
208
|
-
FROM sessions GROUP BY model ORDER BY total DESC;
|
|
209
|
-
|
|
210
220
|
-- Last poll time (freshness check)
|
|
211
221
|
SELECT datetime(MAX(ts)/1000, 'unixepoch', 'localtime') AS last_event
|
|
212
222
|
FROM events;
|
|
@@ -241,6 +251,19 @@ off the machine. `restore.sh` refuses an archive with links in it and a running
|
|
|
241
251
|
SQLite `-wal` cannot mix into the restored state), then resets the permissions. Agent volumes are not included —
|
|
242
252
|
the dashboard's cold backups cover them.
|
|
243
253
|
|
|
254
|
+
### Other tables
|
|
255
|
+
|
|
256
|
+
- `alert_state` — one row per alert key (`lib/alerts.ts`): whether it was sent and resolved, so
|
|
257
|
+
an alert is not sent twice.
|
|
258
|
+
- `tool_calls` and `chat_messages` — created by `lib/db-bootstrap.mjs` and never written by
|
|
259
|
+
any code: `GET /api/tool-calls` reads an always-empty table, nothing reads `chat_messages`.
|
|
260
|
+
Kept until the `events.db` cleanup removes them.
|
|
261
|
+
|
|
262
|
+
`events.db` is Rev4a's own record — what it did to agents (the job tables, created and
|
|
263
|
+
deleted) and what it saw (anomalies, alerts). What agents do is read from OpenClaw: agent
|
|
264
|
+
spend is in `costs.db`; sessions and lineage move to the lineage collector, and with it
|
|
265
|
+
`sessions`, `lineage` and the poll's `spawn`/`complete` events leave this file.
|
|
266
|
+
|
|
244
267
|
## Metrics Database (`metrics.db`)
|
|
245
268
|
|
|
246
269
|
Machine metrics of the host — CPU, RAM, swap, storage — kept apart from `events.db`, so
|
|
@@ -295,6 +318,83 @@ has dropped back under, or once after each daemon start: the state is kept in me
|
|
|
295
318
|
Installs from before this database may still have a `system_metrics` table in
|
|
296
319
|
`events.db`: nothing reads or writes it any more, and it can be dropped.
|
|
297
320
|
|
|
321
|
+
## Costs Database (`costs.db`)
|
|
322
|
+
|
|
323
|
+
What each agent spent, **as its own OpenClaw priced it** — Rev4a computes no cost here.
|
|
324
|
+
Every model in the synced `rev4a` provider block carries its price
|
|
325
|
+
([GATEWAY.md](GATEWAY.md#costs-openclaw-prices-every-call)); each agent's OpenClaw prices
|
|
326
|
+
every call with it, and the daemon copies the figures here, so a stopped or deleted agent
|
|
327
|
+
keeps its history.
|
|
328
|
+
|
|
329
|
+
**Location:** `data/costs.db`, next to `events.db`, resolved like `metrics.db`.
|
|
330
|
+
**Created by** the daemon when it starts, its only writer; the queries are in `lib/agent-costs.ts` (spend) and `lib/cost-reconciliation.ts` (`vendor_balance`), on a read-only handle from `lib/costs-db.ts` — for
|
|
331
|
+
`GET /api/costs/usage`, `GET /api/costs`, `GET /api/costs/agent`, the SSE stream and
|
|
332
|
+
system-health. No file yet → the API answers `available: false`. If the daemon
|
|
333
|
+
cannot open it, it logs `[COSTS] disabled` and the rest of the daemon keeps running.
|
|
334
|
+
**Collection:** 20 s after the daemon starts, then every 5 minutes. First every **running**
|
|
335
|
+
container with an `AGENT_ID` label gets one short call, so that the ones whose index needs
|
|
336
|
+
rebuilding do it in parallel; then, one at a time, two Gateway calls through
|
|
337
|
+
`docker exec … openclaw gateway call` on the agent's port 3000 (token resolved inside the
|
|
338
|
+
container, never on the host's argv), both with `agentScope: "all"` over the last 30 UTC
|
|
339
|
+
days: `usage.cost` (per day, split into input / output / cache read / cache write, and
|
|
340
|
+
the calls it could not price) and `sessions.usage` (per model per day, and the 50 most
|
|
341
|
+
recent sessions). OpenClaw answers from an index of its transcripts that it rebuilds
|
|
342
|
+
after a price change or a restart; an answer whose `cacheStatus` is not `fresh` is never
|
|
343
|
+
stored — asked up to 6 times, 20 s apart (a rebuild took about 60 s on the VPS, from
|
|
344
|
+
4 to 290 transcript files alike), then the previous figures stay and the container's row
|
|
345
|
+
records the error — OpenClaw's own reason ("Gateway not reachable…") or how long the index
|
|
346
|
+
stayed unfresh. An answer missing its lists is refused before anything is deleted. An agent with nothing in the range answers without
|
|
347
|
+
`cacheStatus`, which counts as fresh.
|
|
348
|
+
**Days** are UTC dates, OpenClaw's default and the clock DeepSeek's rates follow.
|
|
349
|
+
**Retention:** the last 30 days are rewritten on every pass (late or repriced calls land in
|
|
350
|
+
the right day); older rows are kept as last read.
|
|
351
|
+
|
|
352
|
+
```sql
|
|
353
|
+
-- One row per container per UTC day with any use: OpenClaw's usage.cost daily entry.
|
|
354
|
+
CREATE TABLE agent_cost_daily (
|
|
355
|
+
container TEXT NOT NULL, date TEXT NOT NULL, -- 'YYYY-MM-DD'
|
|
356
|
+
input INTEGER, output INTEGER, cache_read INTEGER, cache_write INTEGER, total_tokens INTEGER,
|
|
357
|
+
cost REAL, input_cost REAL, output_cost REAL, cache_read_cost REAL, cache_write_cost REAL,
|
|
358
|
+
missing INTEGER, -- calls OpenClaw could not price
|
|
359
|
+
PRIMARY KEY (container, date)
|
|
360
|
+
);
|
|
361
|
+
-- Per model per day: sessions.usage aggregates.modelDaily.
|
|
362
|
+
CREATE TABLE agent_cost_model_daily (
|
|
363
|
+
container TEXT NOT NULL, date TEXT NOT NULL, provider TEXT NOT NULL, model TEXT NOT NULL,
|
|
364
|
+
tokens INTEGER, cost REAL, calls INTEGER,
|
|
365
|
+
PRIMARY KEY (container, date, provider, model)
|
|
366
|
+
);
|
|
367
|
+
-- One row per session seen; its figures are the session's total over the 30-day window.
|
|
368
|
+
CREATE TABLE agent_cost_sessions (
|
|
369
|
+
container TEXT NOT NULL, session_id TEXT NOT NULL,
|
|
370
|
+
session_key TEXT, label TEXT, agent_id TEXT, provider TEXT, model TEXT,
|
|
371
|
+
tokens INTEGER, cost REAL, missing INTEGER,
|
|
372
|
+
first_activity INTEGER, last_activity INTEGER, -- Unix milliseconds
|
|
373
|
+
PRIMARY KEY (container, session_id)
|
|
374
|
+
);
|
|
375
|
+
CREATE INDEX idx_cost_sessions_last ON agent_cost_sessions(last_activity);
|
|
376
|
+
-- The check against the bill: at most once an hour, after a pass, each vendor's own figure
|
|
377
|
+
-- and, read at the same moment, the agents' cumulative priced total for that vendor's
|
|
378
|
+
-- models (agent_cost_model_daily rows whose model starts with '<vendor>/'). Kept 400 days.
|
|
379
|
+
CREATE TABLE vendor_balance (
|
|
380
|
+
ts INTEGER NOT NULL, -- Unix ms
|
|
381
|
+
vendor TEXT NOT NULL, -- 'deepseek' | 'openrouter' (a key must be configured)
|
|
382
|
+
currency TEXT,
|
|
383
|
+
balance REAL, -- DeepSeek: total balance; OpenRouter: credits − usage
|
|
384
|
+
usage REAL, -- OpenRouter: lifetime usage (only grows); NULL for DeepSeek
|
|
385
|
+
agents_cost REAL NOT NULL DEFAULT 0,
|
|
386
|
+
PRIMARY KEY (vendor, ts)
|
|
387
|
+
);
|
|
388
|
+
-- One row per container ever read: its name (AGENT_NAME) and how the last pass went.
|
|
389
|
+
CREATE TABLE agent_cost_sources (
|
|
390
|
+
container TEXT PRIMARY KEY, agent_id TEXT, name TEXT,
|
|
391
|
+
collected_at INTEGER, -- last stored pass, Unix ms
|
|
392
|
+
attempted_at INTEGER, -- last attempt, Unix ms
|
|
393
|
+
error TEXT, -- why the last attempt stored nothing; NULL after a good one
|
|
394
|
+
missing_by_model TEXT -- JSON {"rev4a/<model>": calls} over the 30-day window
|
|
395
|
+
);
|
|
396
|
+
```
|
|
397
|
+
|
|
298
398
|
## Credentials Database (`credentials.db`)
|
|
299
399
|
|
|
300
400
|
The credential hub uses a **separate SQLite file** (`credentials.db`) alongside
|
package/docs/dev/GATEWAY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Gateway Page
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-09-
|
|
3
|
+
> **Last updated:** 2026-09-27
|
|
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
|
|
@@ -69,14 +69,15 @@ against the Rev4a provider proxy using this token. See [`PROVIDERS.md`](PROVIDER
|
|
|
69
69
|
|
|
70
70
|
### Sync Flow
|
|
71
71
|
|
|
72
|
-
When a provider key is saved, a model toggle is changed, Sync All is pressed,
|
|
73
|
-
|
|
72
|
+
When a provider key is saved, a model toggle is changed, Sync All is pressed, Rev4a
|
|
73
|
+
starts, or a time-of-day price changes (see [Costs](#costs-openclaw-prices-every-call)),
|
|
74
|
+
the Gateway:
|
|
74
75
|
|
|
75
76
|
1. **Reads** `models.config.json` and the overrides, and keeps the models that are
|
|
76
77
|
enabled and whose provider has a key. If the file cannot be read and no earlier
|
|
77
78
|
read succeeded, the sync stops here and reports `configError`
|
|
78
79
|
2. **Builds** a `models.providers.rev4a` config block (without `rev4a/` prefix on
|
|
79
|
-
model IDs)
|
|
80
|
+
model IDs), each model carrying the `cost` in force now
|
|
80
81
|
3. **Lists** every container with the `AGENT_ID` Docker label, **running or
|
|
81
82
|
not**. Stopped agents are included so that starting one later does not bring
|
|
82
83
|
back an old catalogue
|
|
@@ -92,6 +93,48 @@ Rev4a starts, the Gateway:
|
|
|
92
93
|
the file's original mode, and apply it when started or resumed
|
|
93
94
|
7. **Does NOT restart** the gateway
|
|
94
95
|
|
|
96
|
+
### Costs: OpenClaw prices every call
|
|
97
|
+
|
|
98
|
+
Every model in the `rev4a` block carries a `cost` — `{ input, output, cacheRead,
|
|
99
|
+
cacheWrite }` in $ per 1M tokens, from `priceAt()` in `lib/model-pricing.ts`. That is the
|
|
100
|
+
field OpenClaw's own cost accounting reads first after an agent's `models.json`, so each
|
|
101
|
+
agent prices every call, session and day itself, with the cache split out, and shows it in
|
|
102
|
+
its own Control UI too. Rev4a computes no cost: the daemon reads OpenClaw's figures into
|
|
103
|
+
`costs.db` and the Costs page (`/costs`) shows them (see
|
|
104
|
+
[DATABASE.md](DATABASE.md#costs-database-costsdb)).
|
|
105
|
+
|
|
106
|
+
- **No price, no `cost`.** A model without an entry in `model-pricing.json` or with a
|
|
107
|
+
dynamic router price (`-1`, OpenRouter Auto) gets none, and OpenClaw counts its calls
|
|
108
|
+
as "missing cost" rather than $0. The Costs page lists them, by model.
|
|
109
|
+
- **Free models** (all-zero price) are written with $0.000001 per 1M tokens for every
|
|
110
|
+
rate: OpenClaw treats an all-zero `cost` as no price and would count every call as
|
|
111
|
+
unpriced. A billion tokens come to $0.001.
|
|
112
|
+
- **Unknown cache rate.** An entry without `cacheRead` falls back to the input rate
|
|
113
|
+
(cached tokens priced as if the cache gave no discount); one without `cacheWrite` to
|
|
114
|
+
1.25 × the input rate — vendors that bill cache writes apart charge more than input
|
|
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.
|
|
119
|
+
- **Time-of-day prices.** DeepSeek bills half its rates off-peak (peak 01:00–04:00 and
|
|
120
|
+
06:00–10:00 UTC, Monday–Friday). `PRICE_SCHEDULES` in `lib/model-pricing.ts` holds
|
|
121
|
+
that schedule; `priceAt()` writes the rate of the moment, and
|
|
122
|
+
`lib/price-schedule-sync.ts` (started by `instrumentation.ts`) checks at most every
|
|
123
|
+
minute — and exactly at a change due within the minute — whether the bands in force
|
|
124
|
+
differ from the ones last written, and runs a full sync when they do; a sync that
|
|
125
|
+
missed an agent is retried every 5 minutes, at most 3 times for one band — an agent busy
|
|
126
|
+
with an update, recreate, restore or edit writes the prices itself when it finishes; the
|
|
127
|
+
bands count as written after any sync, since a sync that misses one agent writes all the
|
|
128
|
+
others — and a new change is written at once, whatever retry was pending. A check rather than one timer to the next
|
|
129
|
+
change: a timer fires late after the machine sleeps, a check a minute after it wakes
|
|
130
|
+
(simulated: asleep 01:05–05:00 UTC across the 04:00 change → synced at 05:00). OpenClaw keeps the cost it recorded at call
|
|
131
|
+
time, so a later change of rate does not reprice a call (verified with a real call,
|
|
132
|
+
priced off-peak and read again after the switch to peak). Chinese public holidays,
|
|
133
|
+
which DeepSeek bills off-peak all day, are priced as peak — a small overestimate.
|
|
134
|
+
- **Calls made before a model had a price** are priced by OpenClaw when read, at the
|
|
135
|
+
rate in force then; OpenClaw reindexes its transcripts after a price change, which is
|
|
136
|
+
why a read can briefly answer `cacheStatus: refreshing`.
|
|
137
|
+
|
|
95
138
|
### Model ID Convention
|
|
96
139
|
|
|
97
140
|
Inside `models.providers.rev4a.models`, model IDs are stored **without** the
|
|
@@ -503,11 +546,12 @@ The OpenRouter payload carries `pricing.prompt`, `pricing.completion`,
|
|
|
503
546
|
same pass.
|
|
504
547
|
|
|
505
548
|
For direct-provider prices, use the provider's own pricing page. DeepSeek's is
|
|
506
|
-
at `api-docs.deepseek.com/quick_start/pricing
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
549
|
+
at `api-docs.deepseek.com/quick_start/pricing`. Record the **peak** rates, and
|
|
550
|
+
the cache-hit rate as `cacheRead` (`cacheWrite` is the input rate for a vendor that
|
|
551
|
+
bills a cache miss as ordinary input); the off-peak discount is applied by
|
|
552
|
+
`PRICE_SCHEDULES` in `lib/model-pricing.ts`, which is where a schedule change goes.
|
|
553
|
+
These prices are what every agent's OpenClaw computes its costs with (see
|
|
554
|
+
[Costs](#costs-openclaw-prices-every-call)).
|
|
511
555
|
|
|
512
556
|
### What to check, in order
|
|
513
557
|
|