@flame0510/project-aether 1.2.0 → 1.4.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 (102) hide show
  1. package/README.md +3 -1
  2. package/agent-templates/README.md +42 -22
  3. package/agent-templates/base-image/Dockerfile +42 -33
  4. package/agent-templates/base-image/entrypoint.sh +67 -12
  5. package/app/agents/BrowserAccessSection.tsx +510 -0
  6. package/app/agents/ChannelManager.tsx +19 -11
  7. package/app/agents/ImageDownloadBanner.tsx +53 -19
  8. package/app/agents/ModelSection.tsx +316 -0
  9. package/app/agents/PageClient.tsx +708 -167
  10. package/app/agents/UpdateSection.tsx +300 -0
  11. package/app/agents/create/PageClient.tsx +11 -49
  12. package/app/agents/create/page.tsx +8 -21
  13. package/app/api/agents/[id]/backup/route.ts +26 -69
  14. package/app/api/agents/[id]/channels/pairing/route.ts +3 -3
  15. package/app/api/agents/[id]/channels/telegram/route.ts +2 -2
  16. package/app/api/agents/[id]/cold-backup/route.ts +56 -0
  17. package/app/api/agents/[id]/devices/route.ts +126 -0
  18. package/app/api/agents/[id]/invite-link/route.ts +53 -0
  19. package/app/api/agents/[id]/lifecycle/route.ts +3 -0
  20. package/app/api/agents/[id]/model/route.ts +113 -0
  21. package/app/api/agents/[id]/open-control-ui/route.ts +58 -0
  22. package/app/api/agents/[id]/recreate/route.ts +33 -187
  23. package/app/api/agents/[id]/restart/route.ts +5 -0
  24. package/app/api/agents/[id]/restore/route.ts +40 -70
  25. package/app/api/agents/[id]/route.ts +38 -169
  26. package/app/api/agents/[id]/update/rollback/route.ts +30 -0
  27. package/app/api/agents/[id]/update/route.ts +50 -0
  28. package/app/api/agents/activity-summary/route.ts +67 -0
  29. package/app/api/agents/create/route.ts +91 -145
  30. package/app/api/agents/devices-summary/route.ts +37 -0
  31. package/app/api/agents/download-image/route.ts +16 -9
  32. package/app/api/agents/image-status/route.ts +31 -111
  33. package/app/api/agents/models-summary/route.ts +163 -0
  34. package/app/api/agents/route.ts +25 -49
  35. package/app/api/agents/token/route.ts +33 -10
  36. package/app/api/assistant/route.ts +37 -16
  37. package/app/api/gateway/agent/route.ts +37 -6
  38. package/app/api/gateway/provider/balance/route.ts +5 -2
  39. package/app/api/gateway/provider/keys.ts +13 -1
  40. package/app/api/gateway/provider/route.ts +43 -12
  41. package/app/api/gateway/sync.ts +335 -76
  42. package/app/api/models/route.ts +28 -34
  43. package/app/api/provider/auth.ts +65 -0
  44. package/app/api/provider/upstream.ts +9 -2
  45. package/app/api/provider/v1/chat/completions/route.ts +22 -16
  46. package/app/api/provider/v1/models/route.ts +26 -133
  47. package/app/api/setup/agent-image/route.ts +14 -42
  48. package/app/components/DashboardToolbar.tsx +1 -1
  49. package/app/components/PulseChat.tsx +25 -39
  50. package/app/components/ui/RemoveButton.tsx +46 -0
  51. package/app/components/ui/Select.tsx +3 -2
  52. package/app/components/ui/index.ts +1 -0
  53. package/app/credentials/PageClient.tsx +2 -2
  54. package/app/gateway/PageClient.tsx +253 -674
  55. package/app/globals.css +8 -0
  56. package/app/lib/models-context.tsx +43 -7
  57. package/app/wizard/useWizard.ts +6 -1
  58. package/bin/rev4a.js +116 -50
  59. package/daemon.js +6 -6
  60. package/docs/ARCHITECTURE.md +110 -12
  61. package/docs/FRONTEND-ARCHITECTURE.md +31 -2
  62. package/docs/REV4A.md +93 -33
  63. package/docs/dev/API-REFERENCE.md +723 -178
  64. package/docs/dev/DATABASE.md +96 -0
  65. package/docs/dev/GATEWAY.md +250 -93
  66. package/docs/dev/PROVIDERS.md +26 -13
  67. package/docs/rag/DATA-FRESHNESS.md +59 -28
  68. package/docs/rag/GLOSSARY.md +27 -16
  69. package/docs/rag/REV4A-OVERVIEW.md +37 -25
  70. package/docs/rag/WHAT-I-CAN-ANSWER.md +10 -8
  71. package/instrumentation.ts +52 -1
  72. package/lib/agent-busy.ts +21 -0
  73. package/lib/agent-devices.ts +361 -0
  74. package/lib/agent-edit-state.ts +108 -0
  75. package/lib/agent-edit.ts +157 -0
  76. package/lib/agent-images.ts +375 -0
  77. package/lib/agent-ports-server.ts +27 -0
  78. package/lib/agent-ports.ts +68 -0
  79. package/lib/agent-readiness.ts +110 -0
  80. package/lib/agent-recreate-state.ts +108 -0
  81. package/lib/agent-recreate.ts +305 -0
  82. package/lib/agent-restore-state.ts +107 -0
  83. package/lib/agent-restore.ts +135 -0
  84. package/lib/agent-setup.ts +66 -17
  85. package/lib/agent-update-state.ts +122 -0
  86. package/lib/agent-update.ts +448 -0
  87. package/lib/agent-versions.json +14 -0
  88. package/lib/agent-versions.ts +80 -0
  89. package/lib/buildAgentImage.ts +88 -290
  90. package/lib/channelManager.ts +153 -64
  91. package/lib/cold-backup.ts +354 -0
  92. package/lib/container-file.ts +27 -0
  93. package/lib/credentials/delivery.ts +3 -3
  94. package/lib/db-bootstrap.mjs +76 -0
  95. package/lib/docker-utils.ts +3 -3
  96. package/lib/model-catalogue.ts +140 -27
  97. package/lib/provider-balance.ts +33 -12
  98. package/lib/rev4a-paths.ts +0 -21
  99. package/model-pricing.json +118 -110
  100. package/models.config.json +27 -12
  101. package/package.json +1 -1
  102. package/app/api/gateway/route.ts +0 -191
@@ -107,6 +107,102 @@ CREATE INDEX idx_metrics_ts ON system_metrics(ts);
107
107
 
108
108
  Rows older than 24 h are pruned automatically by the daemon.
109
109
 
110
+ ### `agent_upgrades` — agent updates and rollbacks
111
+
112
+ ```sql
113
+ CREATE TABLE agent_upgrades (
114
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
115
+ agent_id TEXT NOT NULL,
116
+ status TEXT NOT NULL, -- pending, backing_up, migrating, verifying, done, failed,
117
+ -- interrupted, rolling_back, rolled_back, rollback_failed
118
+ from_version TEXT,
119
+ to_version TEXT NOT NULL,
120
+ backup_file TEXT, -- pre-update cold backup in the rev4a-backups volume
121
+ baseline_json TEXT, -- transcript events per session and cron job names before
122
+ verify_json TEXT, -- the comparison after the update
123
+ error TEXT,
124
+ started_at INTEGER NOT NULL,
125
+ updated_at INTEGER NOT NULL,
126
+ finished_at INTEGER
127
+ );
128
+
129
+ CREATE INDEX idx_agent_upgrades_agent ON agent_upgrades(agent_id, started_at);
130
+ ```
131
+
132
+ One row per update attempt (`lib/agent-update.ts`); a rollback moves the same row on to
133
+ `rolling_back`. Created by `lib/db-bootstrap.mjs`. At startup, rows still in an active
134
+ status are marked `interrupted`. Rows are never pruned.
135
+
136
+ ### `agent_recreates` — agent recreates
137
+
138
+ ```sql
139
+ CREATE TABLE agent_recreates (
140
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
141
+ agent_id TEXT NOT NULL,
142
+ status TEXT NOT NULL, -- backing_up, recreating, done, failed, interrupted
143
+ image TEXT, -- the image the container is rebuilt on
144
+ backup_file TEXT, -- pre-recreate cold backup in the rev4a-backups volume
145
+ error TEXT,
146
+ started_at INTEGER NOT NULL,
147
+ updated_at INTEGER NOT NULL,
148
+ finished_at INTEGER
149
+ );
150
+
151
+ CREATE INDEX idx_agent_recreates_agent ON agent_recreates(agent_id, started_at);
152
+ ```
153
+
154
+ One row per recreate attempt (`lib/agent-recreate.ts`), the same-version counterpart of
155
+ an update: a cold backup, the container rebuilt, the gateway waited for. It is written
156
+ so a page reload or a Rev4a restart mid-way is visible instead of lost
157
+ (`GET /api/agents/[id]/recreate`). At startup, rows still in an active status become
158
+ `interrupted` and the agent is started again once its backup helper has finished. Rows
159
+ are never pruned.
160
+
161
+ ### `agent_restores` — agent restores
162
+
163
+ ```sql
164
+ CREATE TABLE agent_restores (
165
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
166
+ agent_id TEXT NOT NULL,
167
+ status TEXT NOT NULL, -- restoring, done, failed, interrupted
168
+ file TEXT, -- the archive being applied, in the rev4a-backups volume
169
+ error TEXT,
170
+ started_at INTEGER NOT NULL,
171
+ updated_at INTEGER NOT NULL,
172
+ finished_at INTEGER
173
+ );
174
+
175
+ CREATE INDEX idx_agent_restores_agent ON agent_restores(agent_id, started_at);
176
+ ```
177
+
178
+ One row per restore attempt (`lib/agent-restore.ts`): the volume is replaced from the
179
+ archive. Persisted for the same reason as a recreate (`GET /api/agents/[id]/restore`),
180
+ and at startup an active row becomes `interrupted` with the container started again.
181
+ Rows are never pruned.
182
+
183
+ ### `agent_edits` — agent renames and port changes
184
+
185
+ ```sql
186
+ CREATE TABLE agent_edits (
187
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
188
+ agent_id TEXT NOT NULL,
189
+ status TEXT NOT NULL, -- rebuilding, done, failed, interrupted
190
+ display_name TEXT, -- the name being applied, when it was a rename
191
+ port_range TEXT, -- the range being applied, when it was a port change
192
+ error TEXT,
193
+ started_at INTEGER NOT NULL,
194
+ updated_at INTEGER NOT NULL,
195
+ finished_at INTEGER
196
+ );
197
+
198
+ CREATE INDEX idx_agent_edits_agent ON agent_edits(agent_id, started_at);
199
+ ```
200
+
201
+ One row per edit attempt (`lib/agent-edit.ts`): the container is rebuilt with the new
202
+ name/ports, no backup involved. Persisted for the same reason as the others
203
+ (`GET /api/agents/[id]`); at startup an active row becomes `interrupted` with the
204
+ container started again. Rows are never pruned.
205
+
110
206
  ## Useful Queries
111
207
 
112
208
  ```sql
@@ -1,9 +1,10 @@
1
1
  # Gateway Page
2
2
 
3
- > **Last updated:** 2026-07-31
3
+ > **Last updated:** 2026-09-15
4
4
 
5
- The Gateway page (`/gateway`) is the central control panel for managing provider
6
- configurations and agent model assignments across all Docker containers.
5
+ The Gateway page (`/gateway`) is the control panel for provider configuration:
6
+ API keys and the model catalogue offered to every agent container. It does not
7
+ assign models to agents.
7
8
 
8
9
  The Gateway aggregates multiple upstream AI providers (DeepSeek, OpenAI, Anthropic,
9
10
  Kimi, GLM, Qwen, and others) behind a single Rev4a provider. Agent containers see
@@ -15,17 +16,21 @@ at `/api/provider/v1/`.
15
16
  ## Overview
16
17
 
17
18
  ```
18
- Gateway Page
19
- ├── Provider Tab → Manage provider API keys, enable/disable models, push sync
20
- └── Agents Tab → Change primary model per agent container
19
+ Gateway Page → Manage provider API keys, enable/disable models, push sync
20
+ Agent detail → Change the primary model and fallbacks of one agent
21
21
  ```
22
22
 
23
+ The Gateway page is system configuration: which providers are wired up and which
24
+ models the fleet is offered. Assigning a model to one agent is that agent's
25
+ configuration, so it lives in the agent's detail panel (`app/agents/ModelSection.tsx`)
26
+ and not here.
27
+
23
28
  The Gateway talks to each agent container directly via `docker exec`, reading and
24
29
  writing to `/root/.openclaw/openclaw.json` inside the container.
25
30
 
26
31
  ---
27
32
 
28
- ## Provider Tab
33
+ ## Providers
29
34
 
30
35
  Lists all AI providers from `models.config.json` in the project root. Each provider
31
36
  row shows:
@@ -37,13 +42,25 @@ row shows:
37
42
 
38
43
  ### Provider Key Storage
39
44
 
45
+ > **Where these files actually live.** Both sit under the Rev4a data directory,
46
+ > `REV4A_DATA`, which defaults to `~/.config/rev4a/` and moves with
47
+ > `REV4A_DATA_DIR`. Neither is in the repository. They are nested differently and
48
+ > this catches people out, including while writing this document:
49
+ >
50
+ > | file | full path |
51
+ > |---|---|
52
+ > | provider keys | `REV4A_DATA/data/provider-keys.json` |
53
+ > | model overrides | `REV4A_DATA/model-overrides.json` |
54
+ >
55
+ > Paths written below as `data/provider-keys.json` mean the first row.
56
+
40
57
  Provider API keys are stored in `data/provider-keys.json`:
41
58
 
42
59
  ```json
43
60
  {
44
61
  "deepseek": "sk-...",
45
62
  "openrouter": "sk-...",
46
- "rev4a": "ciao"
63
+ "rev4a": "<gateway key>"
47
64
  }
48
65
  ```
49
66
 
@@ -52,19 +69,28 @@ against the Rev4a provider proxy using this token. See [`PROVIDERS.md`](PROVIDER
52
69
 
53
70
  ### Sync Flow
54
71
 
55
- When a provider key is saved or a model toggle is changed, the Gateway:
56
-
57
- 1. **Reads** `models.config.json` to get the full model catalogue
58
- 2. **Filters** enabled models for providers that have a configured API key
59
- 3. **Builds** a `models.providers.rev4a` config block (without `rev4a/` prefix
60
- on model IDs)
61
- 4. **Writes** the block into `/root/.openclaw/openclaw.json` on every agent container
62
- (containers with `AGENT_ID` Docker label)
63
- 5. **Cleans up** stale `models.json` and `auth-profiles.json` files inside each
64
- container
65
- 6. **Writes** the block into `/root/.openclaw/openclaw.json` on every agent container
66
- — **does NOT touch** `agents.defaults.model` or `agents.list[].model`
67
- 7. **Does NOT restart** the gateway — writes are live via file write
72
+ When a provider key is saved, a model toggle is changed, Sync All is pressed, or
73
+ Rev4a starts, the Gateway:
74
+
75
+ 1. **Reads** `models.config.json` and the overrides, and keeps the models that are
76
+ enabled and whose provider has a key. If the file cannot be read and no earlier
77
+ read succeeded, the sync stops here and reports `configError`
78
+ 2. **Builds** a `models.providers.rev4a` config block (without `rev4a/` prefix on
79
+ model IDs)
80
+ 3. **Lists** every container with the `AGENT_ID` Docker label, **running or
81
+ not**. Stopped agents are included so that starting one later does not bring
82
+ back an old catalogue
83
+ 4. **Reads** each container's `/root/.openclaw/openclaw.json` — through
84
+ `docker exec` when running, through `docker cp` otherwise (stopped, paused, created), since `exec`
85
+ cannot reach them. A read that fails, or returns an empty object, stops
86
+ there: **nothing is written** and the container is reported in `sync.failed`
87
+ 5. **Cleans up** stale `models.json` and `auth-profiles.json` — running containers
88
+ only, since it needs `exec`
89
+ 6. **Writes** the file back with only `models.providers.rev4a` replaced — **does NOT
90
+ touch** `agents.defaults.model` or `agents.list[].model`. Running containers get
91
+ it through `docker exec` and hot-apply it; the others through `docker cp`, with
92
+ the file's original mode, and apply it when started or resumed
93
+ 7. **Does NOT restart** the gateway
68
94
 
69
95
  ### Model ID Convention
70
96
 
@@ -80,7 +106,7 @@ Inside `models.providers.rev4a.models`, model IDs are stored **without** the
80
106
  "api": "openai-completions",
81
107
  "apiKey": "***",
82
108
  "models": [
83
- { "id": "deepseek/deepseek-v4-flash", "name": "DeepSeek V4 Flash" },
109
+ { "id": "deepseek/deepseek-flash", "name": "DeepSeek Flash" },
84
110
  { "id": "deepseek/deepseek-v4-pro", "name": "DeepSeek V4 Pro" }
85
111
  ]
86
112
  }
@@ -90,7 +116,7 @@ Inside `models.providers.rev4a.models`, model IDs are stored **without** the
90
116
  ```
91
117
 
92
118
  OpenClaw automatically prefixes model IDs with the provider name at runtime,
93
- producing `rev4a/deepseek/deepseek-v4-flash`. The `mode` field is **not** set —
119
+ producing `rev4a/deepseek/deepseek-flash`. The `mode` field is **not** set —
94
120
  OpenClaw defaults to merging config models with auto-discovered models.
95
121
 
96
122
  ### Model Catalogue
@@ -124,107 +150,119 @@ containers.
124
150
 
125
151
  ---
126
152
 
127
- ## Agents Tab
128
-
129
- Lists all agent containers discovered from Docker (containers with `AGENT_ID`
130
- label). Each agent row shows:
131
-
132
- - Agent name and container name
133
- - Current primary model
134
- - "Change Model" button
135
-
136
- ### Change Model Flow
137
-
138
- Clicking "Change Model" opens an inline form with:
139
-
140
- 1. **Primary model select** — dropdown of all enabled models (filtered to
141
- configured providers only)
142
-
143
- On save:
144
-
145
- 1. **PUT /api/gateway/agent** writes the model config to the container's
146
- `openclaw.json`:
147
- - `agents.defaults.model.primary` and `agents.list[0].model.primary` written
148
- with format `rev4a/<provider>/<model>`
149
- - No fallbacks are set unless explicitly provided
150
- 2. **No restart** — writes are live via `docker exec node -e`
151
-
152
- ### Model Select Filtering
153
-
154
- The model select in the Agents tab only shows models whose provider has a
155
- configured API key in `data/provider-keys.json`. This ensures users can only
156
- select models that are actually available.
153
+ ## Per-agent model assignment
154
+
155
+ Lives in the agent detail panel, not on this page. `app/agents/ModelSection.tsx`
156
+ reads `GET /api/agents/[id]/model` for the current primary and fallbacks, and the
157
+ enabled catalogue from `GET /api/models`, which returns only models whose provider
158
+ has a configured API key in `<data dir>/provider-keys.json`. Note this page does
159
+ **not** apply that filter — `GET /api/gateway/provider` returns the whole catalogue
160
+ so you can add a key and enable its models in one pass. The filter is applied by
161
+ `/api/models` and by the sync.
162
+
163
+ Saving calls `PUT /api/gateway/agent`, which writes the container's
164
+ `openclaw.json`:
165
+
166
+ - `agents.defaults.model`, and the entry in `agents.list` whose `id` is `main` —
167
+ found by id, not by position, and skipped entirely if no such entry exists. The
168
+ primary is stored as `rev4a/<provider>/<model>`
169
+ - `fallbacks` omitted and `fallbacks: []` are different requests: omitted leaves the
170
+ container's existing list untouched, `[]` clears it. The Model panel is the only
171
+ UI that sets them; the create wizard does not
172
+ - **no restart** — `agents` is in OpenClaw's hot-reload column, so the write
173
+ applies on the agent's next turn. Exception on OpenClaw 2026.7.1-2: a Telegram
174
+ channel keeps the configuration it had when it started, so Telegram replies stay on
175
+ the old model until the container is restarted. The Control UI picks the change up
176
+ at once. OpenClaw 9.x reads the configuration per message
177
+
178
+ The agent list shows each agent's current model as a chip, fed by
179
+ `GET /api/agents/models-summary`. The red states are split by remedy, because they
180
+ look alike and need different actions:
181
+
182
+ - `NOT IN CATALOGUE` — the id is gone from the catalogue: pick another model;
183
+ - `NOT ENABLED` — it exists but is not offered, and the proxy refuses it: pick another model;
184
+ - `OUT OF SYNC` — the gateway offers the agent's primary, but this container's synced
185
+ copy does not have it: run Sync All.
186
+
187
+ Amber `DEPRECATED` marks a working model on a retired name. All of these describe the
188
+ primary model; a neutral `NO FALLBACK` is the one chip about the fallback list, shown
189
+ when it is empty. Every agent gets them, stopped ones included (read from the volume
190
+ with `docker cp`). The agent's Model panel repeats the red and amber distinction in
191
+ its warning.
157
192
 
158
193
  ---
159
194
 
160
195
  ## API Endpoints
161
196
 
162
- ### `GET /api/gateway`
163
-
164
- Returns live Gateway status from all agent containers. Reads agent model configs
165
- via `openclaw models status --json` inside each container.
166
-
167
- **Response:**
168
- ```json
169
- {
170
- "agents": {
171
- "total": 1,
172
- "list": [
173
- {
174
- "containerName": "openclaw-atlas",
175
- "agentId": "atlas",
176
- "agentName": "Atlas",
177
- "defaultModel": "rev4a/deepseek/deepseek-v4-flash",
178
- "configured": true
179
- }
180
- ]
181
- }
182
- }
183
- ```
184
-
185
197
  ### `PUT /api/gateway/provider`
186
198
 
187
199
  Enables or disables a single model in the catalogue, then syncs every agent.
188
200
  Only writes `models.providers.rev4a` — does NOT touch model references.
189
201
 
190
- **Body:** `{ "modelId": "deepseek/deepseek-chat", "enabled": true }` — both
202
+ **Body:** `{ "modelId": "deepseek/deepseek-flash", "enabled": true }` — both
191
203
  required. Missing either returns `400`; an unknown `modelId` returns `404`.
204
+ If Docker cannot be reached the toggle is refused with `503` and nothing is saved.
192
205
 
193
- **Response:**
206
+ **Response:** `status` reports the toggle, saved before the sync runs; `sync`
207
+ reports whether every agent received it.
194
208
  ```json
195
209
  {
196
210
  "status": "ok",
197
- "modelId": "deepseek/deepseek-chat",
198
- "enabled": true,
211
+ "modelId": "deepseek/deepseek-flash",
212
+ "enabled": false,
199
213
  "provider": "deepseek",
200
- "sync": ["Active models: 2 across 1 agent(s)"]
214
+ "sync": { "ok": true, "activeModels": 2, "total": 2,
215
+ "synced": ["agent_2a3c3a07", "agent_9253eee3"], "stopped": [], "failed": [],
216
+ "summary": "Synced 2 of 2 agent(s)." }
201
217
  }
202
218
  ```
203
219
 
220
+ Two failures, handled differently on purpose:
221
+
222
+ - **Docker unreachable** — refused before anything is written: `503`, nothing saved,
223
+ the checkbox does not move, so the checkboxes never disagree with every agent.
224
+ - **An agent under a cold backup** is skipped: writing its config while tar reads the
225
+ volume would put a half-changed file in the archive. It is listed in `sync.failed`
226
+ with the reason; the next sync catches it up.
227
+ - **Some containers fail once Docker answers** — the toggle stands and `sync.failed`
228
+ lists them; the page shows it in amber, and an agent whose primary is affected shows
229
+ `OUT OF SYNC`. Rolling
230
+ the override back would not restore consistency, because the containers that did
231
+ receive the change would keep it.
232
+
233
+ Full shape in
234
+ [API-REFERENCE.md](API-REFERENCE.md).
235
+
204
236
  ### `PUT /api/gateway/agent`
205
237
 
206
- Updates model config for a single agent container (primary model only).
238
+ Updates the primary model and fallbacks of a single agent container.
207
239
 
208
240
  **Body:**
209
241
  ```json
210
242
  {
211
243
  "containerName": "openclaw-atlas",
212
- "model": "deepseek/deepseek-v4-flash"
244
+ "model": "deepseek/deepseek-flash",
245
+ "fallbacks": ["deepseek/deepseek-v4-pro"]
213
246
  }
214
247
  ```
215
248
 
216
- **Response:**
249
+ Omit `fallbacks` to keep the container's current list; send `[]` to clear it.
250
+
251
+ **Response:** what was written, prefixed.
217
252
  ```json
218
253
  {
219
254
  "status": "ok",
220
255
  "agent": "openclaw-atlas",
221
256
  "model": {
222
- "primary": "rev4a/deepseek/deepseek-v4-flash"
257
+ "primary": "rev4a/deepseek/deepseek-flash",
258
+ "fallbacks": ["rev4a/deepseek/deepseek-v4-pro"]
223
259
  },
224
260
  "verify": { "ok": true, "bytes": 2058 }
225
261
  }
226
262
  ```
227
263
 
264
+ Full semantics in [API-REFERENCE.md](API-REFERENCE.md).
265
+
228
266
  ### `GET /api/gateway/provider`
229
267
 
230
268
  Returns current provider configuration state.
@@ -245,8 +283,8 @@ OpenRouter to find out.
245
283
  "baseUrl": "https://api.deepseek.com",
246
284
  "models": [
247
285
  {
248
- "id": "deepseek/deepseek-v4-flash",
249
- "name": "DeepSeek V4 Flash",
286
+ "id": "deepseek/deepseek-flash",
287
+ "name": "DeepSeek Flash",
250
288
  "enabled": true,
251
289
  "pricing": { "input": 0.05, "output": 0.10 },
252
290
  "pricingLive": true,
@@ -327,17 +365,136 @@ Restart the Rev4a server (via systemd).
327
365
  4. **No gateway restart after model sync** — The sync module writes `models.providers`
328
366
  to the file directly without restarting the gateway.
329
367
 
368
+ The model `input` list follows each agent's OpenClaw version
369
+ (`lib/agent-versions.json`, resolved by `containerVersionSync()`): the newest release
370
+ accepts `text | image | audio | video`, while `2026.7.1-2` accepts only `text | image`,
371
+ and one unknown value makes it discard the whole generated catalogue
372
+ (`model catalog load issue`). That is why the previous version stays in the table
373
+ while agents still run it.
374
+
330
375
  5. **Entrypoint does not generate openclaw.json** — The entrypoint resolves the
331
376
  auth token and starts the gateway with `--allow-unconfigured`; OpenClaw
332
377
  generates its own default config on first boot. All post-creation config
333
378
  (controlUi, model refs, providers) is written by the create route
334
379
  (`POST /api/agents/create`) after the gateway is fully ready.
335
- The entrypoint also runs `openclaw doctor --non-interactive` **before** the
336
- gateway, but only when the OpenClaw version changed since last boot
337
- (tracked in `/root/.openclaw/.last-version`) — safe migrations only, no restart.
380
+ Before the gateway starts, the entrypoint runs `openclaw doctor --fix --non-interactive`
381
+ when the OpenClaw version changed since the last successful run (tracked in
382
+ `/root/.openclaw/.last-version`), and fills in the image defaults that are missing:
383
+ plugin load path, web search provider, heartbeat off. See
384
+ [REV4A.md](../REV4A.md#agent-templates).
338
385
 
339
386
  6. **Model ref written after gateway readiness** — When the wizard creates an agent,
340
- the create route waits for gateway health check, then writes
341
- `agents.defaults.model.primary` + fallbacks and `gateway.controlUi.allowedOrigins`,
342
- then calls `syncAgent(name)`. The Gateway sync (`PUT /api/gateway/provider`)
387
+ the create route waits for the gateway to finish starting (`waitForGatewayReady`), then writes
388
+ `agents.defaults.model.primary` + fallbacks and the Control UI origin policy (`gateway.controlUi`),
389
+ then writes the provider block from `buildRev4aProviderConfig()` with `openclaw config patch --stdin`. The Gateway sync (`PUT /api/gateway/provider`)
343
390
  never touches model references — only `models.providers` is synced.
391
+
392
+ ---
393
+
394
+ ## Maintaining the model catalogue
395
+
396
+ Two files, both tracked in the repo, both maintained by hand:
397
+
398
+ | File | Holds |
399
+ |---|---|
400
+ | `models.config.json` | the catalogue: id, name, provider, `enabled`, `modality`, deprecation flags |
401
+ | `model-pricing.json` | price per 1M tokens, keyed by the same ids |
402
+
403
+ They drift, because upstream catalogues change without telling anyone. Refresh
404
+ them on a schedule that suits you, and always before shipping a release.
405
+
406
+ ### Where the truth lives
407
+
408
+ Do not copy figures from blog posts or search results — they go stale and
409
+ contradict each other. Two sources answer authoritatively:
410
+
411
+ ```bash
412
+ # OpenRouter: ids, prices and input modalities for every model, no auth
413
+ curl -s https://openrouter.ai/api/v1/models
414
+
415
+ # DeepSeek: the definitive list of callable model names
416
+ curl -s https://api.deepseek.com/models -H "Authorization: Bearer $DEEPSEEK_KEY"
417
+ ```
418
+
419
+ The OpenRouter payload carries `pricing.prompt`, `pricing.completion`,
420
+ `pricing.input_cache_read` (multiply by 1e6 for per-million figures) and
421
+ `architecture.input_modalities`, so prices and modalities can be checked in the
422
+ same pass.
423
+
424
+ For direct-provider prices, use the provider's own pricing page. DeepSeek's is
425
+ at `api-docs.deepseek.com/quick_start/pricing`, and note it charges **peak and
426
+ off-peak rates** (peak: 01:00–04:00 and 06:00–10:00 UTC, Mon–Fri; off-peak is
427
+ half). `model-pricing.json` holds one figure per model and cannot express that,
428
+ nor cache-hit rates — **record the peak rate**, so the dashboard overstates
429
+ spend rather than understating it.
430
+
431
+ ### What to check, in order
432
+
433
+ 1. **Models that no longer exist upstream.** Set `enabled: false`; do not delete
434
+ an id an agent might still be configured with — see below.
435
+ 2. **Prices that drifted.** Compare every `openrouter/*` entry against the live
436
+ API; direct providers against their pricing page.
437
+ 3. **Modalities.** `architecture.input_modalities` is authoritative. Compare the
438
+ *set*, not the string: `text+image` and `image+text` are the same thing, and
439
+ rewriting one into the other produces a diff full of noise.
440
+ 4. **New models worth carrying.** Add with `enabled` reflecting policy, not
441
+ availability — see the DeepSeek rule below.
442
+ 5. **Retired names.** Mark them, do not remove them yet.
443
+
444
+ ### The deprecation lifecycle
445
+
446
+ Upstream sometimes retires a model *name* while keeping it working, redirected to
447
+ a successor and billed at the successor's rates.
448
+
449
+ Handle it in two releases:
450
+
451
+ ```json5
452
+ // release N — flag it. It still works, and it stays selectable.
453
+ { "id": "provider/retired-model", "deprecated": true }
454
+
455
+ // release N+1 — remove it. Agents still on it show NOT IN CATALOGUE, in red.
456
+ ```
457
+
458
+ The gap between the two releases is what gives anyone using it a chance to move.
459
+ Removing in one step turns a warning into an outage.
460
+
461
+ `deprecated` surfaces in two places: a `DEPRECATED` chip on the agent card, and a
462
+ `(deprecated)` tag on the option in the primary and fallback pickers of the
463
+ agent's Model panel. The tag rides on the option label, so a native select keeps
464
+ showing it once the model is chosen.
465
+
466
+ There is no field for the successor: the redirect is the provider's, and nothing here
467
+ acts on it. Where the successor matters, say so in prose.
468
+
469
+ Do **not** encode deprecation in `name`. The name is the model's name; the flag
470
+ renders itself, and putting it in both produces `DeepSeek V4 Flash (legacy →
471
+ Flash) (deprecated)`.
472
+
473
+ **Never remove an id in the same release you deprecate it**, and never remove one
474
+ without checking who uses it: an agent whose primary model leaves the catalogue
475
+ fails on its next turn, and with an empty fallback list it fails hard. The agent
476
+ cards surface this — `NOT IN CATALOGUE` in red — but only after the fact.
477
+
478
+ ### Policy vs availability
479
+
480
+ `enabled` expresses **what this deployment should offer**, not what exists. Both
481
+ DeepSeek direct and OpenRouter carry the DeepSeek models; this deployment uses
482
+ the direct provider, so the OpenRouter copies ship disabled.
483
+
484
+ User toggles live separately in `<data dir>/model-overrides.json` and win over
485
+ `enabled`. When a default changes to match an existing override,
486
+ `toggleModelOverride` drops the now-redundant key on the next interaction, so the
487
+ overrides file stays limited to genuine divergence.
488
+
489
+ ### After editing
490
+
491
+ - both files must stay valid JSON, and every `enabled` model needs a price entry
492
+ - remove pricing rows for ids you removed from the catalogue — orphans accumulate
493
+ - read the catalogue only through `lib/model-catalogue.ts`: `loadModelsConfig()` for
494
+ the catalogue with overrides, `loadOfferedModels()` for what the deployment offers,
495
+ `isModelOffered()` to decide whether to serve a request. Nothing else in the app
496
+ reads `models.config.json`; `scripts/refresh-model-pricing.mjs` rewrites it offline,
497
+ and the backup and restore scripts copy it
498
+ - if the file cannot be read, the last copy this process read successfully is used;
499
+ with no earlier copy the sync refuses to push. Orphan overrides are pruned only
500
+ when a toggle is saved against a freshly read catalogue
@@ -1,6 +1,6 @@
1
1
  # Provider Gateway & Key Management
2
2
 
3
- > **Last updated:** 2026-08-28
3
+ > **Last updated:** 2026-09-14
4
4
 
5
5
  This document covers the Rev4a Provider Gateway (proxy), the provider key
6
6
  endpoints, and how agent containers authenticate against the Rev4a proxy.
@@ -19,7 +19,7 @@ Agent Container Rev4a Gateway Upstream API
19
19
  │ │ │
20
20
  │ Authorization: Bearer <rev4a-key> │
21
21
  │ POST /api/provider/v1/chat/completions │
22
- │ model: rev4a/deepseek-deepseek-v4-flash │
22
+ │ model: rev4a/deepseek/deepseek-flash │
23
23
  │────────────────────────>│ │
24
24
  │ │ POST https://api.deepseek.com/v1/chat/completions
25
25
  │ │ Authorization: Bearer <deepseek-key>
@@ -67,7 +67,7 @@ not by auto-discovery.
67
67
  "total": 9,
68
68
  "data": [
69
69
  {
70
- "id": "deepseek/deepseek-v4-flash",
70
+ "id": "deepseek/deepseek-flash",
71
71
  "object": "model",
72
72
  "created": 1700000000,
73
73
  "owned_by": "deepseek"
@@ -95,11 +95,15 @@ name is parsed to extract the provider and model ID.
95
95
 
96
96
  | Model alias | Upstream provider | Upstream model |
97
97
  |---|---|---|
98
- | `rev4a/deepseek-v4-flash` | `deepseek` | `deepseek-v4-flash` |
98
+ | `rev4a/deepseek-flash` | `deepseek` | `deepseek-flash` |
99
99
  | `rev4a/deepseek-v4-pro` | `deepseek` | `deepseek-v4-pro` |
100
- | `rev4a/kimi-k3` | `kimi` | `kimi-k3` |
101
- | `rev4a/glm-5.2` | `glm` | `glm-5.2` |
102
- | `rev4a/qwen3.7-plus` | `qwen` | `qwen3.7-plus` |
100
+
101
+ This map is a short list of two-segment shortcuts, not the general mechanism.
102
+ Every other model resolves through step 3 below, from its three-segment id:
103
+ `rev4a/kimi/kimi-k3` resolves to provider `kimi`, model `kimi-k3`. A two-segment
104
+ `rev4a/kimi-k3` is **not** an alias and does not resolve — `rev4a` is not a
105
+ provider key, so it falls through to the `deepseek` default with an unusable
106
+ model string.
103
107
 
104
108
  **Model parsing fallback:**
105
109
  1. Check `request.provider` field
@@ -114,7 +118,7 @@ The upstream module (`app/api/provider/upstream.ts`) handles this via a per-prov
114
118
  **Request body** (OpenAI-compatible):
115
119
  ```json
116
120
  {
117
- "model": "rev4a/deepseek-v4-flash",
121
+ "model": "rev4a/deepseek-flash",
118
122
  "messages": [{"role": "user", "content": "Hello"}]
119
123
  }
120
124
  ```
@@ -145,8 +149,13 @@ Store a provider API key, or re-run the sync without changing keys.
145
149
  **Body:** `{ "provider": "deepseek", "apiKey": "sk-..." }`, or `{ "_syncOnly": true }`
146
150
  to push the current configuration to every agent container without touching any key.
147
151
 
148
- **Response:** `{ "status": "ok", ... }`. Unknown provider names are rejected with
149
- `400` and the list of known providers.
152
+ **Response:** `{ "status": "ok", "provider": "...", "configured": true, "sync": { ... } }`.
153
+ The key is saved before the sync runs, so `status` reports the save and `sync`
154
+ reports whether every agent received it; its shape is documented under
155
+ `PUT /api/gateway/provider` in [API-REFERENCE.md](API-REFERENCE.md). With
156
+ `_syncOnly` the sync *is* the operation: a failed or partial sync returns `502` with
157
+ `status: "error"` and `error` set to the summary. Unknown provider names are
158
+ rejected with `400` and the list of known providers.
150
159
 
151
160
  **Auth:** browser cookie or bearer token
152
161
 
@@ -154,9 +163,13 @@ to push the current configuration to every agent container without touching any
154
163
 
155
164
  Enable or disable a single model in the catalogue.
156
165
 
157
- **Body:** `{ "modelId": "deepseek/deepseek-chat", "enabled": true }` — both fields
166
+ **Body:** `{ "modelId": "deepseek/deepseek-flash", "enabled": true }` — both fields
158
167
  required. Unknown `modelId` returns `404`.
159
168
 
169
+ **Response:** as for the key save — `status` for the toggle, `sync` for whether
170
+ the agents received it. If Docker cannot be reached the toggle is refused with `503` and
171
+ nothing is saved; a key save in the same situation is saved and the failure reported.
172
+
160
173
  **Auth:** browser cookie or bearer token
161
174
 
162
175
  There is no `DELETE`: a provider key is cleared by storing an empty one.
@@ -177,7 +190,7 @@ Inside each agent container, the `openclaw.json` file has this structure for the
177
190
  "api": "openai-completions",
178
191
  "apiKey": "<gateway-token>",
179
192
  "models": [
180
- { "id": "deepseek/deepseek-v4-flash", "name": "DeepSeek V4 Flash" },
193
+ { "id": "deepseek/deepseek-flash", "name": "DeepSeek Flash" },
181
194
  { "id": "deepseek/deepseek-v4-pro", "name": "DeepSeek V4 Pro" }
182
195
  ]
183
196
  }
@@ -186,7 +199,7 @@ Inside each agent container, the `openclaw.json` file has this structure for the
186
199
  "agents": {
187
200
  "defaults": {
188
201
  "model": {
189
- "primary": "rev4a/deepseek/deepseek-v4-flash"
202
+ "primary": "rev4a/deepseek/deepseek-flash"
190
203
  }
191
204
  }
192
205
  }