@flame0510/project-aether 1.1.15 → 1.3.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 (60) hide show
  1. package/README.md +2 -1
  2. package/app/agents/ModelSection.tsx +313 -0
  3. package/app/agents/PageClient.tsx +83 -4
  4. package/app/agents/create/page.tsx +8 -21
  5. package/app/api/agents/[id]/model/route.ts +113 -0
  6. package/app/api/agents/[id]/recreate/route.ts +10 -34
  7. package/app/api/agents/[id]/route.ts +10 -29
  8. package/app/api/agents/create/route.ts +59 -57
  9. package/app/api/agents/models-summary/route.ts +163 -0
  10. package/app/api/assistant/route.ts +36 -15
  11. package/app/api/credentials/[id]/sync/route.ts +3 -3
  12. package/app/api/credentials/detect/route.ts +126 -176
  13. package/app/api/credentials/route.ts +3 -0
  14. package/app/api/gateway/agent/route.ts +23 -6
  15. package/app/api/gateway/provider/keys.ts +13 -1
  16. package/app/api/gateway/provider/route.ts +43 -12
  17. package/app/api/gateway/sync.ts +248 -72
  18. package/app/api/models/route.ts +28 -34
  19. package/app/api/provider/auth.ts +65 -0
  20. package/app/api/provider/upstream.ts +9 -2
  21. package/app/api/provider/v1/chat/completions/route.ts +22 -16
  22. package/app/api/provider/v1/models/route.ts +26 -133
  23. package/app/components/PulseChat.tsx +25 -39
  24. package/app/components/Skeleton.tsx +132 -0
  25. package/app/components/ui/RemoveButton.tsx +46 -0
  26. package/app/components/ui/Select.tsx +3 -2
  27. package/app/components/ui/index.ts +1 -0
  28. package/app/credentials/PageClient.tsx +461 -140
  29. package/app/credentials/loading.tsx +19 -5
  30. package/app/gateway/PageClient.tsx +257 -673
  31. package/app/globals.css +8 -0
  32. package/app/lib/models-context.tsx +43 -7
  33. package/app/wizard/useWizard.ts +6 -1
  34. package/bin/rev4a.js +73 -9
  35. package/docs/ARCHITECTURE.md +92 -33
  36. package/docs/FRONTEND-ARCHITECTURE.md +36 -6
  37. package/docs/REV4A.md +62 -30
  38. package/docs/dev/API-REFERENCE.md +490 -227
  39. package/docs/dev/DATABASE.md +8 -3
  40. package/docs/dev/GATEWAY.md +236 -92
  41. package/docs/dev/PROVIDERS.md +44 -44
  42. package/docs/rag/DATA-FRESHNESS.md +57 -28
  43. package/docs/rag/GLOSSARY.md +20 -18
  44. package/docs/rag/REV4A-OVERVIEW.md +28 -32
  45. package/docs/rag/WHAT-I-CAN-ANSWER.md +10 -12
  46. package/instrumentation.ts +9 -1
  47. package/lib/agent-readiness.ts +110 -0
  48. package/lib/channelManager.ts +64 -22
  49. package/lib/container-file.ts +27 -0
  50. package/lib/credentials/delivery.ts +212 -119
  51. package/lib/credentials/detect.ts +229 -97
  52. package/lib/credentials/providers.ts +38 -7
  53. package/lib/credentials/vault.ts +78 -13
  54. package/lib/docker-exec.ts +50 -14
  55. package/lib/model-catalogue.ts +140 -27
  56. package/lib/rev4a-paths.ts +0 -21
  57. package/model-pricing.json +118 -110
  58. package/models.config.json +27 -12
  59. package/package.json +1 -1
  60. package/app/api/gateway/route.ts +0 -191
@@ -1,6 +1,6 @@
1
1
  # Rev4a — Database Reference
2
2
 
3
- > **Last updated:** 2026-06-30
3
+ > **Last updated:** 2026-08-28
4
4
 
5
5
  Rev4a uses a single SQLite file (`events.db`) in WAL mode.
6
6
 
@@ -182,7 +182,9 @@ CREATE TABLE credential_secrets (
182
182
  );
183
183
  ```
184
184
 
185
- Payloads are stored as JSON strings. In a future release they will be AES-256-GCM encrypted.
185
+ `payload` holds the secret as raw JSON — **not encrypted**. Anything that can read
186
+ this file, or reach `POST /api/credentials/[id]/reveal` with a valid session, has
187
+ the secrets in the clear.
186
188
 
187
189
  ### `credential_audit_log` — append-only audit trail
188
190
 
@@ -197,7 +199,10 @@ CREATE TABLE credential_audit_log (
197
199
  );
198
200
  ```
199
201
 
200
- Actions: `create`, `update`, `delete`, `sync`, `reveal`.
202
+ Actions: `create`, `update`, `delete`, `sync`, `desync`, `reveal`.
203
+
204
+ Nothing reads `credential_audit_log`: it is written by `appendAudit()` and exposed
205
+ through no route or view.
201
206
 
202
207
  ---
203
208
 
@@ -1,9 +1,10 @@
1
1
  # Gateway Page
2
2
 
3
- > **Last updated:** 2026-07-31
3
+ > **Last updated:** 2026-09-14
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,12 +116,12 @@ 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
97
123
 
98
- The model catalogue lives in [`models.config.json`](../models.config.json) at the project root.
124
+ The model catalogue lives in [`models.config.json`](../../models.config.json) at the project root.
99
125
  This file is **tracked in the repository** and serves as the default model configuration
100
126
  for anyone cloning the project. It defines all known models across all providers:
101
127
 
@@ -124,105 +150,113 @@ 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
174
+
175
+ The agent list shows each agent's current model as a chip, fed by
176
+ `GET /api/agents/models-summary`. The red states are split by remedy, because they
177
+ look alike and need different actions:
178
+
179
+ - `NOT IN CATALOGUE` — the id is gone from the catalogue: pick another model;
180
+ - `NOT ENABLED` — it exists but is not offered, and the proxy refuses it: pick another model;
181
+ - `OUT OF SYNC` — the gateway offers the agent's primary, but this container's synced
182
+ copy does not have it: run Sync All.
183
+
184
+ Amber `DEPRECATED` marks a working model on a retired name. All of these describe the
185
+ primary model; a neutral `NO FALLBACK` is the one chip about the fallback list, shown
186
+ when it is empty. Every agent gets them, stopped ones included (read from the volume
187
+ with `docker cp`). The agent's Model panel repeats the red and amber distinction in
188
+ its warning.
157
189
 
158
190
  ---
159
191
 
160
192
  ## API Endpoints
161
193
 
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
194
  ### `PUT /api/gateway/provider`
186
195
 
187
- Triggers a full provider model sync to all agent containers. Only writes
188
- `models.providers.rev4a` — does NOT touch model references.
196
+ Enables or disables a single model in the catalogue, then syncs every agent.
197
+ Only writes `models.providers.rev4a` — does NOT touch model references.
189
198
 
190
- **Body:** none (reads current provider state from `models.config.json` and
191
- `data/provider-keys.json`)
199
+ **Body:** `{ "modelId": "deepseek/deepseek-flash", "enabled": true }` — both
200
+ required. Missing either returns `400`; an unknown `modelId` returns `404`.
201
+ If Docker cannot be reached the toggle is refused with `503` and nothing is saved.
192
202
 
193
- **Response:**
203
+ **Response:** `status` reports the toggle, saved before the sync runs; `sync`
204
+ reports whether every agent received it.
194
205
  ```json
195
206
  {
196
207
  "status": "ok",
197
- "activeModels": 2,
198
- "results": ["Active models: 2 across 1 agent(s)"]
208
+ "modelId": "deepseek/deepseek-flash",
209
+ "enabled": false,
210
+ "provider": "deepseek",
211
+ "sync": { "ok": true, "activeModels": 2, "total": 2,
212
+ "synced": ["agent_2a3c3a07", "agent_9253eee3"], "stopped": [], "failed": [],
213
+ "summary": "Synced 2 of 2 agent(s)." }
199
214
  }
200
215
  ```
201
216
 
217
+ Two failures, handled differently on purpose:
218
+
219
+ - **Docker unreachable** — refused before anything is written: `503`, nothing saved,
220
+ the checkbox does not move, so the checkboxes never disagree with every agent.
221
+ - **Some containers fail once Docker answers** — the toggle stands and `sync.failed`
222
+ lists them; the page shows it in amber, and an agent whose primary is affected shows
223
+ `OUT OF SYNC`. Rolling
224
+ the override back would not restore consistency, because the containers that did
225
+ receive the change would keep it.
226
+
227
+ Full shape in
228
+ [API-REFERENCE.md](API-REFERENCE.md).
229
+
202
230
  ### `PUT /api/gateway/agent`
203
231
 
204
- Updates model config for a single agent container (primary model only).
232
+ Updates the primary model and fallbacks of a single agent container.
205
233
 
206
234
  **Body:**
207
235
  ```json
208
236
  {
209
237
  "containerName": "openclaw-atlas",
210
- "model": "deepseek/deepseek-v4-flash"
238
+ "model": "deepseek/deepseek-flash",
239
+ "fallbacks": ["deepseek/deepseek-v4-pro"]
211
240
  }
212
241
  ```
213
242
 
214
- **Response:**
243
+ Omit `fallbacks` to keep the container's current list; send `[]` to clear it.
244
+
245
+ **Response:** what was written, prefixed.
215
246
  ```json
216
247
  {
217
248
  "status": "ok",
218
249
  "agent": "openclaw-atlas",
219
250
  "model": {
220
- "primary": "rev4a/deepseek/deepseek-v4-flash"
251
+ "primary": "rev4a/deepseek/deepseek-flash",
252
+ "fallbacks": ["rev4a/deepseek/deepseek-v4-pro"]
221
253
  },
222
254
  "verify": { "ok": true, "bytes": 2058 }
223
255
  }
224
256
  ```
225
257
 
258
+ Full semantics in [API-REFERENCE.md](API-REFERENCE.md).
259
+
226
260
  ### `GET /api/gateway/provider`
227
261
 
228
262
  Returns current provider configuration state.
@@ -243,8 +277,8 @@ OpenRouter to find out.
243
277
  "baseUrl": "https://api.deepseek.com",
244
278
  "models": [
245
279
  {
246
- "id": "deepseek/deepseek-v4-flash",
247
- "name": "DeepSeek V4 Flash",
280
+ "id": "deepseek/deepseek-flash",
281
+ "name": "DeepSeek Flash",
248
282
  "enabled": true,
249
283
  "pricing": { "input": 0.05, "output": 0.10 },
250
284
  "pricingLive": true,
@@ -335,7 +369,117 @@ Restart the Rev4a server (via systemd).
335
369
  (tracked in `/root/.openclaw/.last-version`) — safe migrations only, no restart.
336
370
 
337
371
  6. **Model ref written after gateway readiness** — When the wizard creates an agent,
338
- the create route waits for gateway health check, then writes
372
+ the create route waits for the gateway to finish starting (`waitForGatewayReady`), then writes
339
373
  `agents.defaults.model.primary` + fallbacks and `gateway.controlUi.allowedOrigins`,
340
- then calls `syncAgent(name)`. The Gateway sync (`PUT /api/gateway/provider`)
374
+ then writes the provider block from `buildRev4aProviderConfig()` with `openclaw config patch --stdin`. The Gateway sync (`PUT /api/gateway/provider`)
341
375
  never touches model references — only `models.providers` is synced.
376
+
377
+ ---
378
+
379
+ ## Maintaining the model catalogue
380
+
381
+ Two files, both tracked in the repo, both maintained by hand:
382
+
383
+ | File | Holds |
384
+ |---|---|
385
+ | `models.config.json` | the catalogue: id, name, provider, `enabled`, `modality`, deprecation flags |
386
+ | `model-pricing.json` | price per 1M tokens, keyed by the same ids |
387
+
388
+ They drift, because upstream catalogues change without telling anyone. Refresh
389
+ them on a schedule that suits you, and always before shipping a release.
390
+
391
+ ### Where the truth lives
392
+
393
+ Do not copy figures from blog posts or search results — they go stale and
394
+ contradict each other. Two sources answer authoritatively:
395
+
396
+ ```bash
397
+ # OpenRouter: ids, prices and input modalities for every model, no auth
398
+ curl -s https://openrouter.ai/api/v1/models
399
+
400
+ # DeepSeek: the definitive list of callable model names
401
+ curl -s https://api.deepseek.com/models -H "Authorization: Bearer $DEEPSEEK_KEY"
402
+ ```
403
+
404
+ The OpenRouter payload carries `pricing.prompt`, `pricing.completion`,
405
+ `pricing.input_cache_read` (multiply by 1e6 for per-million figures) and
406
+ `architecture.input_modalities`, so prices and modalities can be checked in the
407
+ same pass.
408
+
409
+ For direct-provider prices, use the provider's own pricing page. DeepSeek's is
410
+ at `api-docs.deepseek.com/quick_start/pricing`, and note it charges **peak and
411
+ off-peak rates** (peak: 01:00–04:00 and 06:00–10:00 UTC, Mon–Fri; off-peak is
412
+ half). `model-pricing.json` holds one figure per model and cannot express that,
413
+ nor cache-hit rates — **record the peak rate**, so the dashboard overstates
414
+ spend rather than understating it.
415
+
416
+ ### What to check, in order
417
+
418
+ 1. **Models that no longer exist upstream.** Set `enabled: false`; do not delete
419
+ an id an agent might still be configured with — see below.
420
+ 2. **Prices that drifted.** Compare every `openrouter/*` entry against the live
421
+ API; direct providers against their pricing page.
422
+ 3. **Modalities.** `architecture.input_modalities` is authoritative. Compare the
423
+ *set*, not the string: `text+image` and `image+text` are the same thing, and
424
+ rewriting one into the other produces a diff full of noise.
425
+ 4. **New models worth carrying.** Add with `enabled` reflecting policy, not
426
+ availability — see the DeepSeek rule below.
427
+ 5. **Retired names.** Mark them, do not remove them yet.
428
+
429
+ ### The deprecation lifecycle
430
+
431
+ Upstream sometimes retires a model *name* while keeping it working, redirected to
432
+ a successor and billed at the successor's rates.
433
+
434
+ Handle it in two releases:
435
+
436
+ ```json5
437
+ // release N — flag it. It still works, and it stays selectable.
438
+ { "id": "provider/retired-model", "deprecated": true }
439
+
440
+ // release N+1 — remove it. Agents still on it show NOT IN CATALOGUE, in red.
441
+ ```
442
+
443
+ The gap between the two releases is what gives anyone using it a chance to move.
444
+ Removing in one step turns a warning into an outage.
445
+
446
+ `deprecated` surfaces in two places: a `DEPRECATED` chip on the agent card, and a
447
+ `(deprecated)` tag on the option in the primary and fallback pickers of the
448
+ agent's Model panel. The tag rides on the option label, so a native select keeps
449
+ showing it once the model is chosen.
450
+
451
+ There is no field for the successor: the redirect is the provider's, and nothing here
452
+ acts on it. Where the successor matters, say so in prose.
453
+
454
+ Do **not** encode deprecation in `name`. The name is the model's name; the flag
455
+ renders itself, and putting it in both produces `DeepSeek V4 Flash (legacy →
456
+ Flash) (deprecated)`.
457
+
458
+ **Never remove an id in the same release you deprecate it**, and never remove one
459
+ without checking who uses it: an agent whose primary model leaves the catalogue
460
+ fails on its next turn, and with an empty fallback list it fails hard. The agent
461
+ cards surface this — `NOT IN CATALOGUE` in red — but only after the fact.
462
+
463
+ ### Policy vs availability
464
+
465
+ `enabled` expresses **what this deployment should offer**, not what exists. Both
466
+ DeepSeek direct and OpenRouter carry the DeepSeek models; this deployment uses
467
+ the direct provider, so the OpenRouter copies ship disabled.
468
+
469
+ User toggles live separately in `<data dir>/model-overrides.json` and win over
470
+ `enabled`. When a default changes to match an existing override,
471
+ `toggleModelOverride` drops the now-redundant key on the next interaction, so the
472
+ overrides file stays limited to genuine divergence.
473
+
474
+ ### After editing
475
+
476
+ - both files must stay valid JSON, and every `enabled` model needs a price entry
477
+ - remove pricing rows for ids you removed from the catalogue — orphans accumulate
478
+ - read the catalogue only through `lib/model-catalogue.ts`: `loadModelsConfig()` for
479
+ the catalogue with overrides, `loadOfferedModels()` for what the deployment offers,
480
+ `isModelOffered()` to decide whether to serve a request. Nothing else in the app
481
+ reads `models.config.json`; `scripts/refresh-model-pricing.mjs` rewrites it offline,
482
+ and the backup and restore scripts copy it
483
+ - if the file cannot be read, the last copy this process read successfully is used;
484
+ with no earlier copy the sync refuses to push. Orphan overrides are pruned only
485
+ when a toggle is saved against a freshly read catalogue
@@ -1,14 +1,10 @@
1
1
  # Provider Gateway & Key Management
2
2
 
3
- > **Last updated:** 2026-07-31
3
+ > **Last updated:** 2026-09-14
4
4
 
5
- This document covers the Rev4a Provider Gateway (proxy),
6
- the vault key endpoint, and how agent containers authenticate against the
7
- Rev4a proxy.
8
-
9
- > **Note:** The old `/providers` UI page and its API routes (`/api/providers/*`,
10
- > `/api/agent-providers`) have been removed (2026-07-27). Provider/API-key
11
- > configuration is now centralized in the [Gateway](../GATEWAY.md) page.
5
+ This document covers the Rev4a Provider Gateway (proxy), the provider key
6
+ endpoints, and how agent containers authenticate against the Rev4a proxy.
7
+ Provider and API-key configuration lives in the [Gateway](GATEWAY.md) page.
12
8
 
13
9
  ---
14
10
 
@@ -23,7 +19,7 @@ Agent Container Rev4a Gateway Upstream API
23
19
  │ │ │
24
20
  │ Authorization: Bearer <rev4a-key> │
25
21
  │ POST /api/provider/v1/chat/completions │
26
- │ model: rev4a/deepseek-deepseek-v4-flash │
22
+ │ model: rev4a/deepseek/deepseek-flash │
27
23
  │────────────────────────>│ │
28
24
  │ │ POST https://api.deepseek.com/v1/chat/completions
29
25
  │ │ Authorization: Bearer <deepseek-key>
@@ -71,7 +67,7 @@ not by auto-discovery.
71
67
  "total": 9,
72
68
  "data": [
73
69
  {
74
- "id": "deepseek/deepseek-v4-flash",
70
+ "id": "deepseek/deepseek-flash",
75
71
  "object": "model",
76
72
  "created": 1700000000,
77
73
  "owned_by": "deepseek"
@@ -99,11 +95,15 @@ name is parsed to extract the provider and model ID.
99
95
 
100
96
  | Model alias | Upstream provider | Upstream model |
101
97
  |---|---|---|
102
- | `rev4a/deepseek-v4-flash` | `deepseek` | `deepseek-v4-flash` |
98
+ | `rev4a/deepseek-flash` | `deepseek` | `deepseek-flash` |
103
99
  | `rev4a/deepseek-v4-pro` | `deepseek` | `deepseek-v4-pro` |
104
- | `rev4a/kimi-k3` | `kimi` | `kimi-k3` |
105
- | `rev4a/glm-5.2` | `glm` | `glm-5.2` |
106
- | `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.
107
107
 
108
108
  **Model parsing fallback:**
109
109
  1. Check `request.provider` field
@@ -118,7 +118,7 @@ The upstream module (`app/api/provider/upstream.ts`) handles this via a per-prov
118
118
  **Request body** (OpenAI-compatible):
119
119
  ```json
120
120
  {
121
- "model": "rev4a/deepseek-v4-flash",
121
+ "model": "rev4a/deepseek-flash",
122
122
  "messages": [{"role": "user", "content": "Hello"}]
123
123
  }
124
124
  ```
@@ -130,50 +130,50 @@ The proxy authenticates using the provider's real API key from
130
130
 
131
131
  ## Vault & Provider Key Endpoints
132
132
 
133
- ### `GET /api/vault/provider/key`
133
+ ### `GET /api/gateway/provider`
134
134
 
135
- Returns the full API key for a provider. Used by the Providers page SHOW KEY flow.
135
+ Provider state and the model catalogue.
136
136
 
137
137
  **Query params:**
138
138
 
139
139
  | Param | Required | Description |
140
140
  |---|---|---|
141
- | `provider` | Yes | Provider identifier (e.g. `openai`, `deepseek`) |
142
- | `agent` | No | Docker container name for agent-targeted reads; omitting reads the VPS host |
141
+ | `summary` | No | `1` returns only which providers have a key — no models, no pricing. The full response waits on live pricing from OpenRouter, a network round trip costing seconds; callers that need just the boolean must not pay for it. |
143
142
 
144
- **Auth:** browser cookie
143
+ **Auth:** browser cookie or bearer token
145
144
 
146
- **Response (200):**
147
- ```json
148
- {
149
- "provider": "deepseek",
150
- "apiKey": "sk-...full-key...",
151
- "masked": "sk-...be03",
152
- "source": "local"
153
- }
154
- ```
145
+ ### `POST /api/gateway/provider`
146
+
147
+ Store a provider API key, or re-run the sync without changing keys.
155
148
 
156
- **Source values:**
157
- - `local` — read from `data/provider-keys.json` on the VPS host
158
- - `container` — read from inside the agent container config
149
+ **Body:** `{ "provider": "deepseek", "apiKey": "sk-..." }`, or `{ "_syncOnly": true }`
150
+ to push the current configuration to every agent container without touching any key.
159
151
 
160
- **Error (404):** `{ "error": "not_found" }` — provider has no key
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.
161
159
 
162
- ### `POST /api/vault/provider`
160
+ **Auth:** browser cookie or bearer token
163
161
 
164
- Add or update a stored provider credential in the vault.
162
+ ### `PUT /api/gateway/provider`
165
163
 
166
- **Body:** `{ "provider": "deepseek", "apiKey": "sk-...", "baseUrl": "https://api.deepseek.com" }`
164
+ Enable or disable a single model in the catalogue.
167
165
 
168
- **Response:** `{ "status": "ok", "provider": "deepseek", "masked": "sk-...be03", "updatedAt": 1749200000000 }`
166
+ **Body:** `{ "modelId": "deepseek/deepseek-flash", "enabled": true }` — both fields
167
+ required. Unknown `modelId` returns `404`.
169
168
 
170
- ### `DELETE /api/vault/provider`
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.
171
172
 
172
- Remove a stored provider credential from the vault.
173
+ **Auth:** browser cookie or bearer token
173
174
 
174
- **Body:** `{ "provider": "deepseek" }`
175
+ There is no `DELETE`: a provider key is cleared by storing an empty one.
175
176
 
176
- **Response:** `{ "status": "removed" }`
177
177
 
178
178
  ---
179
179
 
@@ -190,7 +190,7 @@ Inside each agent container, the `openclaw.json` file has this structure for the
190
190
  "api": "openai-completions",
191
191
  "apiKey": "<gateway-token>",
192
192
  "models": [
193
- { "id": "deepseek/deepseek-v4-flash", "name": "DeepSeek V4 Flash" },
193
+ { "id": "deepseek/deepseek-flash", "name": "DeepSeek Flash" },
194
194
  { "id": "deepseek/deepseek-v4-pro", "name": "DeepSeek V4 Pro" }
195
195
  ]
196
196
  }
@@ -199,7 +199,7 @@ Inside each agent container, the `openclaw.json` file has this structure for the
199
199
  "agents": {
200
200
  "defaults": {
201
201
  "model": {
202
- "primary": "rev4a/deepseek/deepseek-v4-flash"
202
+ "primary": "rev4a/deepseek/deepseek-flash"
203
203
  }
204
204
  }
205
205
  }
@@ -218,7 +218,7 @@ Key points:
218
218
 
219
219
  ## Security Notes
220
220
 
221
- - The `GET /api/vault/provider/key` endpoint calls `requireAuthJWT` directly
221
+ - The `GET /api/gateway/provider` endpoint calls `requireAuthJWT` directly
222
222
  (browser cookie or bearer token) — API routes are never covered by `proxy.ts`
223
223
  (it explicitly exempts `/api/*`), so each route must guard itself. Only
224
224
  authenticated users can reveal keys.