@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.
- package/README.md +2 -1
- package/app/agents/ModelSection.tsx +313 -0
- package/app/agents/PageClient.tsx +83 -4
- package/app/agents/create/page.tsx +8 -21
- package/app/api/agents/[id]/model/route.ts +113 -0
- package/app/api/agents/[id]/recreate/route.ts +10 -34
- package/app/api/agents/[id]/route.ts +10 -29
- package/app/api/agents/create/route.ts +59 -57
- package/app/api/agents/models-summary/route.ts +163 -0
- package/app/api/assistant/route.ts +36 -15
- package/app/api/credentials/[id]/sync/route.ts +3 -3
- package/app/api/credentials/detect/route.ts +126 -176
- package/app/api/credentials/route.ts +3 -0
- package/app/api/gateway/agent/route.ts +23 -6
- package/app/api/gateway/provider/keys.ts +13 -1
- package/app/api/gateway/provider/route.ts +43 -12
- package/app/api/gateway/sync.ts +248 -72
- package/app/api/models/route.ts +28 -34
- package/app/api/provider/auth.ts +65 -0
- package/app/api/provider/upstream.ts +9 -2
- package/app/api/provider/v1/chat/completions/route.ts +22 -16
- package/app/api/provider/v1/models/route.ts +26 -133
- package/app/components/PulseChat.tsx +25 -39
- package/app/components/Skeleton.tsx +132 -0
- package/app/components/ui/RemoveButton.tsx +46 -0
- package/app/components/ui/Select.tsx +3 -2
- package/app/components/ui/index.ts +1 -0
- package/app/credentials/PageClient.tsx +461 -140
- package/app/credentials/loading.tsx +19 -5
- package/app/gateway/PageClient.tsx +257 -673
- package/app/globals.css +8 -0
- package/app/lib/models-context.tsx +43 -7
- package/app/wizard/useWizard.ts +6 -1
- package/bin/rev4a.js +73 -9
- package/docs/ARCHITECTURE.md +92 -33
- package/docs/FRONTEND-ARCHITECTURE.md +36 -6
- package/docs/REV4A.md +62 -30
- package/docs/dev/API-REFERENCE.md +490 -227
- package/docs/dev/DATABASE.md +8 -3
- package/docs/dev/GATEWAY.md +236 -92
- package/docs/dev/PROVIDERS.md +44 -44
- package/docs/rag/DATA-FRESHNESS.md +57 -28
- package/docs/rag/GLOSSARY.md +20 -18
- package/docs/rag/REV4A-OVERVIEW.md +28 -32
- package/docs/rag/WHAT-I-CAN-ANSWER.md +10 -12
- package/instrumentation.ts +9 -1
- package/lib/agent-readiness.ts +110 -0
- package/lib/channelManager.ts +64 -22
- package/lib/container-file.ts +27 -0
- package/lib/credentials/delivery.ts +212 -119
- package/lib/credentials/detect.ts +229 -97
- package/lib/credentials/providers.ts +38 -7
- package/lib/credentials/vault.ts +78 -13
- package/lib/docker-exec.ts +50 -14
- package/lib/model-catalogue.ts +140 -27
- package/lib/rev4a-paths.ts +0 -21
- package/model-pricing.json +118 -110
- package/models.config.json +27 -12
- package/package.json +1 -1
- package/app/api/gateway/route.ts +0 -191
package/docs/dev/DATABASE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a — Database Reference
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
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
|
-
|
|
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
|
|
package/docs/dev/GATEWAY.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Gateway Page
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
3
|
+
> **Last updated:** 2026-09-14
|
|
4
4
|
|
|
5
|
-
The Gateway page (`/gateway`) is the
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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": "
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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-
|
|
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-
|
|
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`](
|
|
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
|
-
##
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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:**
|
|
191
|
-
|
|
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
|
-
"
|
|
198
|
-
"
|
|
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
|
|
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-
|
|
238
|
+
"model": "deepseek/deepseek-flash",
|
|
239
|
+
"fallbacks": ["deepseek/deepseek-v4-pro"]
|
|
211
240
|
}
|
|
212
241
|
```
|
|
213
242
|
|
|
214
|
-
|
|
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-
|
|
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-
|
|
247
|
-
"name": "DeepSeek
|
|
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
|
|
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
|
|
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
|
package/docs/dev/PROVIDERS.md
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# Provider Gateway & Key Management
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
3
|
+
> **Last updated:** 2026-09-14
|
|
4
4
|
|
|
5
|
-
This document covers the Rev4a Provider Gateway (proxy),
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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-
|
|
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-
|
|
98
|
+
| `rev4a/deepseek-flash` | `deepseek` | `deepseek-flash` |
|
|
103
99
|
| `rev4a/deepseek-v4-pro` | `deepseek` | `deepseek-v4-pro` |
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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-
|
|
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/
|
|
133
|
+
### `GET /api/gateway/provider`
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
Provider state and the model catalogue.
|
|
136
136
|
|
|
137
137
|
**Query params:**
|
|
138
138
|
|
|
139
139
|
| Param | Required | Description |
|
|
140
140
|
|---|---|---|
|
|
141
|
-
| `
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
**
|
|
157
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
160
|
+
**Auth:** browser cookie or bearer token
|
|
163
161
|
|
|
164
|
-
|
|
162
|
+
### `PUT /api/gateway/provider`
|
|
165
163
|
|
|
166
|
-
|
|
164
|
+
Enable or disable a single model in the catalogue.
|
|
167
165
|
|
|
168
|
-
**
|
|
166
|
+
**Body:** `{ "modelId": "deepseek/deepseek-flash", "enabled": true }` — both fields
|
|
167
|
+
required. Unknown `modelId` returns `404`.
|
|
169
168
|
|
|
170
|
-
|
|
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
|
-
|
|
173
|
+
**Auth:** browser cookie or bearer token
|
|
173
174
|
|
|
174
|
-
|
|
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-
|
|
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-
|
|
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/
|
|
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.
|