@flame0510/project-aether 1.1.12 → 1.1.13
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 +4 -1
- package/app/agents/PageClient.tsx +84 -22
- package/app/api/agents/create/route.ts +15 -3
- package/app/api/agents/download-image/route.ts +2 -0
- package/app/api/agents/image-status/route.ts +2 -0
- package/app/api/agents/route.ts +1 -36
- package/app/api/agents/token/route.ts +4 -1
- package/app/api/auth/check/route.ts +2 -23
- package/app/api/auth/login/route.ts +3 -35
- package/app/api/auth/logout/route.ts +1 -1
- package/app/api/auth/status/route.ts +2 -21
- package/app/api/config/env/route.ts +7 -3
- package/app/api/config/restart/route.ts +2 -0
- package/app/api/containers/route.ts +3 -34
- package/app/api/crons/route.ts +2 -0
- package/app/api/debug/route.ts +2 -0
- package/app/api/envcheck/route.ts +3 -1
- package/app/api/gateway/provider/route.ts +43 -5
- package/app/api/gateway/route.ts +2 -0
- package/app/api/models/route.ts +2 -0
- package/app/api/setup/agent-image/route.ts +8 -3
- package/app/api/setup/password/route.ts +32 -37
- package/app/api/setup/restart/route.ts +8 -2
- package/app/api/skills/delete/route.js +1 -1
- package/app/api/skills/promote/route.js +1 -1
- package/app/api/skills/route.js +1 -1
- package/app/api/skills/save/route.js +18 -5
- package/app/api/update-check/route.ts +4 -23
- package/app/api/wizard/complete/route.ts +7 -3
- package/app/api/wizard/reset/route.ts +8 -3
- package/app/api/wizard/status/route.ts +8 -5
- package/app/components/AuthGuard.tsx +29 -8
- package/app/components/ui/ModalityIcons.tsx +157 -0
- package/app/components/ui/index.ts +1 -0
- package/app/gateway/PageClient.tsx +104 -16
- package/bin/rev4a.js +35 -24
- package/docs/ARCHITECTURE.md +31 -13
- package/docs/FRONTEND-ARCHITECTURE.md +1 -0
- package/docs/REV4A.md +36 -6
- package/docs/dev/API-REFERENCE.md +9 -4
- package/docs/dev/GATEWAY.md +42 -1
- package/docs/dev/PROVIDERS.md +4 -3
- package/docs/rag/REV4A-OVERVIEW.md +1 -1
- package/instrumentation.ts +18 -0
- package/lib/agent-setup.ts +8 -9
- package/lib/buildAgentImage.ts +5 -1
- package/lib/db.ts +0 -10
- package/lib/docker-socket.ts +121 -0
- package/lib/openrouter-pricing.ts +184 -0
- package/lib/rev4a-auth.d.ts +4 -1
- package/lib/rev4a-auth.js +68 -20
- package/lib/rev4a-paths.ts +19 -0
- package/model-pricing.json +50 -50
- package/models.config.json +2 -2
- package/package.json +4 -1
- package/proxy.ts +68 -0
- package/scripts/refresh-model-pricing.mjs +171 -0
- package/lib/auth.ts +0 -28
- package/lib/requireAuth.tsx +0 -38
package/model-pricing.json
CHANGED
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"output": 8.0
|
|
41
41
|
},
|
|
42
42
|
"openrouter/auto": {
|
|
43
|
-
"input": -1,
|
|
44
|
-
"output": -1
|
|
43
|
+
"input": -1.0,
|
|
44
|
+
"output": -1.0
|
|
45
45
|
},
|
|
46
46
|
"google/gemini-3.1-pro-preview": {
|
|
47
47
|
"input": 2.0,
|
|
@@ -124,8 +124,8 @@
|
|
|
124
124
|
"output": 2.2
|
|
125
125
|
},
|
|
126
126
|
"glm/glm-4.7-flash": {
|
|
127
|
-
"input": 0,
|
|
128
|
-
"output": 0
|
|
127
|
+
"input": 0.0,
|
|
128
|
+
"output": 0.0
|
|
129
129
|
},
|
|
130
130
|
"glm/glm-4.7-flashx": {
|
|
131
131
|
"input": 0.07,
|
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
"output": 4.0
|
|
137
137
|
},
|
|
138
138
|
"glm/glm-4.6v-flash": {
|
|
139
|
-
"input": 0,
|
|
140
|
-
"output": 0
|
|
139
|
+
"input": 0.0,
|
|
140
|
+
"output": 0.0
|
|
141
141
|
},
|
|
142
142
|
"qwen/qwen3.7-max": {
|
|
143
143
|
"input": 0.83,
|
|
@@ -160,36 +160,36 @@
|
|
|
160
160
|
"output": 0.28
|
|
161
161
|
},
|
|
162
162
|
"groq/llama-3.1-8b-instant": {
|
|
163
|
-
"input": 0,
|
|
164
|
-
"output": 0
|
|
163
|
+
"input": 0.0,
|
|
164
|
+
"output": 0.0
|
|
165
165
|
},
|
|
166
166
|
"groq/llama-3.3-70b-versatile": {
|
|
167
|
-
"input": 0,
|
|
168
|
-
"output": 0
|
|
167
|
+
"input": 0.0,
|
|
168
|
+
"output": 0.0
|
|
169
169
|
},
|
|
170
170
|
"openrouter/deepseek/deepseek-v4-flash": {
|
|
171
|
-
"input": 0.
|
|
172
|
-
"output": 0.
|
|
171
|
+
"input": 0.04886,
|
|
172
|
+
"output": 0.09772
|
|
173
173
|
},
|
|
174
174
|
"openrouter/google/gemini-2.5-flash": {
|
|
175
|
-
"input": 0.
|
|
176
|
-
"output":
|
|
175
|
+
"input": 0.3,
|
|
176
|
+
"output": 2.5
|
|
177
177
|
},
|
|
178
178
|
"openrouter/google/gemini-2.5-pro": {
|
|
179
179
|
"input": 1.25,
|
|
180
|
-
"output":
|
|
180
|
+
"output": 10.0
|
|
181
181
|
},
|
|
182
182
|
"openrouter/anthropic/claude-sonnet-4.6": {
|
|
183
183
|
"input": 3.0,
|
|
184
184
|
"output": 15.0
|
|
185
185
|
},
|
|
186
186
|
"openrouter/deepseek/deepseek-v4-pro": {
|
|
187
|
-
"input": 0.
|
|
188
|
-
"output": 0.
|
|
187
|
+
"input": 0.396894,
|
|
188
|
+
"output": 0.793788
|
|
189
189
|
},
|
|
190
190
|
"openrouter/qwen/qwen3.7-max": {
|
|
191
|
-
"input": 1.
|
|
192
|
-
"output": 4.
|
|
191
|
+
"input": 1.475,
|
|
192
|
+
"output": 4.425
|
|
193
193
|
},
|
|
194
194
|
"openrouter/qwen/qwen3.7-plus": {
|
|
195
195
|
"input": 0.32,
|
|
@@ -200,16 +200,16 @@
|
|
|
200
200
|
"output": 0.13
|
|
201
201
|
},
|
|
202
202
|
"openrouter/qwen/qwen3.6-plus": {
|
|
203
|
-
"input": 0.
|
|
203
|
+
"input": 0.325,
|
|
204
204
|
"output": 1.95
|
|
205
205
|
},
|
|
206
206
|
"openrouter/qwen/qwen3.6-flash": {
|
|
207
|
-
"input": 0.
|
|
208
|
-
"output": 1.
|
|
207
|
+
"input": 0.1875,
|
|
208
|
+
"output": 1.125
|
|
209
209
|
},
|
|
210
210
|
"openrouter/qwen/qwen3.6-27b": {
|
|
211
|
-
"input": 0.
|
|
212
|
-
"output":
|
|
211
|
+
"input": 0.6,
|
|
212
|
+
"output": 3.6
|
|
213
213
|
},
|
|
214
214
|
"openrouter/qwen/qwen3.6-35b-a3b": {
|
|
215
215
|
"input": 0.14,
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
"output": 3.9
|
|
229
229
|
},
|
|
230
230
|
"openrouter/qwen/qwen3-next-80b-a3b-instruct": {
|
|
231
|
-
"input": 0.
|
|
231
|
+
"input": 0.1,
|
|
232
232
|
"output": 1.1
|
|
233
233
|
},
|
|
234
234
|
"openrouter/qwen/qwen3-next-80b-a3b-thinking": {
|
|
@@ -244,20 +244,20 @@
|
|
|
244
244
|
"output": 6.0
|
|
245
245
|
},
|
|
246
246
|
"openrouter/qwen/qwen3.6-max-preview": {
|
|
247
|
-
"input": 1.
|
|
248
|
-
"output": 6.
|
|
247
|
+
"input": 1.027,
|
|
248
|
+
"output": 6.162
|
|
249
249
|
},
|
|
250
250
|
"openrouter/minimax/minimax-m3": {
|
|
251
251
|
"input": 0.3,
|
|
252
252
|
"output": 1.2
|
|
253
253
|
},
|
|
254
254
|
"openrouter/moonshotai/kimi-k2.7-code": {
|
|
255
|
-
"input": 0.
|
|
256
|
-
"output": 3.
|
|
255
|
+
"input": 0.67,
|
|
256
|
+
"output": 3.4
|
|
257
257
|
},
|
|
258
258
|
"openrouter/moonshotai/kimi-k2.6": {
|
|
259
|
-
"input": 0.
|
|
260
|
-
"output":
|
|
259
|
+
"input": 0.95,
|
|
260
|
+
"output": 4.0
|
|
261
261
|
},
|
|
262
262
|
"openrouter/moonshotai/kimi-k2": {
|
|
263
263
|
"input": 0.57,
|
|
@@ -272,16 +272,16 @@
|
|
|
272
272
|
"output": 15.0
|
|
273
273
|
},
|
|
274
274
|
"openrouter/z-ai/glm-5.2": {
|
|
275
|
-
"input": 0.
|
|
276
|
-
"output":
|
|
275
|
+
"input": 0.966,
|
|
276
|
+
"output": 3.036
|
|
277
277
|
},
|
|
278
278
|
"openrouter/z-ai/glm-5.1": {
|
|
279
|
-
"input": 0.
|
|
280
|
-
"output": 3.
|
|
279
|
+
"input": 0.966,
|
|
280
|
+
"output": 3.036
|
|
281
281
|
},
|
|
282
282
|
"openrouter/z-ai/glm-5": {
|
|
283
|
-
"input": 0.
|
|
284
|
-
"output":
|
|
283
|
+
"input": 0.6,
|
|
284
|
+
"output": 1.92
|
|
285
285
|
},
|
|
286
286
|
"openrouter/z-ai/glm-5v-turbo": {
|
|
287
287
|
"input": 1.2,
|
|
@@ -308,15 +308,15 @@
|
|
|
308
308
|
"output": 0.3
|
|
309
309
|
},
|
|
310
310
|
"openrouter/tencent/hy3": {
|
|
311
|
-
"input": 0.
|
|
312
|
-
"output": 0.
|
|
311
|
+
"input": 0.132,
|
|
312
|
+
"output": 0.528
|
|
313
313
|
},
|
|
314
314
|
"openrouter/tencent/hy3-preview": {
|
|
315
|
-
"input": 0.
|
|
316
|
-
"output": 0.
|
|
315
|
+
"input": 0.18,
|
|
316
|
+
"output": 0.6
|
|
317
317
|
},
|
|
318
318
|
"openrouter/xiaomi/mimo-v2.5-pro": {
|
|
319
|
-
"input": 0.
|
|
319
|
+
"input": 0.435,
|
|
320
320
|
"output": 0.87
|
|
321
321
|
},
|
|
322
322
|
"openrouter/xiaomi/mimo-v2.5": {
|
|
@@ -384,7 +384,7 @@
|
|
|
384
384
|
"output": 3.2
|
|
385
385
|
},
|
|
386
386
|
"openrouter/nvidia/nemotron-3-super-120b-a12b": {
|
|
387
|
-
"input": 0.
|
|
387
|
+
"input": 0.085,
|
|
388
388
|
"output": 0.4
|
|
389
389
|
},
|
|
390
390
|
"openrouter/nvidia/nemotron-3-ultra-550b-a55b": {
|
|
@@ -396,7 +396,7 @@
|
|
|
396
396
|
"output": 3.0
|
|
397
397
|
},
|
|
398
398
|
"openrouter/thinkingmachines/inkling": {
|
|
399
|
-
"input":
|
|
399
|
+
"input": 0.95,
|
|
400
400
|
"output": 4.05
|
|
401
401
|
},
|
|
402
402
|
"openrouter/relace/relace-apply-3": {
|
|
@@ -416,8 +416,8 @@
|
|
|
416
416
|
"output": 12.0
|
|
417
417
|
},
|
|
418
418
|
"openrouter/google/gemini-3.6-flash": {
|
|
419
|
-
"input":
|
|
420
|
-
"output":
|
|
419
|
+
"input": 0.75,
|
|
420
|
+
"output": 3.75
|
|
421
421
|
},
|
|
422
422
|
"openrouter/anthropic/claude-opus-5": {
|
|
423
423
|
"input": 5.0,
|
|
@@ -436,12 +436,12 @@
|
|
|
436
436
|
"output": 25.0
|
|
437
437
|
},
|
|
438
438
|
"openrouter/openai/gpt-5.6-sol": {
|
|
439
|
-
"input":
|
|
440
|
-
"output":
|
|
439
|
+
"input": 2.0,
|
|
440
|
+
"output": 10.0
|
|
441
441
|
},
|
|
442
442
|
"openrouter/openai/gpt-5.6-luna": {
|
|
443
|
-
"input": 0.
|
|
444
|
-
"output":
|
|
443
|
+
"input": 0.2,
|
|
444
|
+
"output": 1.2
|
|
445
445
|
},
|
|
446
446
|
"openrouter/openai/gpt-5.4-mini": {
|
|
447
447
|
"input": 0.75,
|
package/models.config.json
CHANGED
|
@@ -796,7 +796,7 @@
|
|
|
796
796
|
"name": "Kimi K3 (OR)",
|
|
797
797
|
"provider": "openrouter",
|
|
798
798
|
"enabled": false,
|
|
799
|
-
"modality": "text+image->text"
|
|
799
|
+
"modality": "text+image+video->text"
|
|
800
800
|
},
|
|
801
801
|
{
|
|
802
802
|
"id": "openrouter/amazon/nova-premier-v1",
|
|
@@ -841,4 +841,4 @@
|
|
|
841
841
|
"modality": "text+image->text"
|
|
842
842
|
}
|
|
843
843
|
]
|
|
844
|
-
}
|
|
844
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flame0510/project-aether",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
4
4
|
"description": "Rev4a — Revolution for Agents. OpenClaw agent fleet orchestrator.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"lineage.js",
|
|
39
39
|
"terminal-ws-server.js",
|
|
40
40
|
"package.json",
|
|
41
|
+
"proxy.ts",
|
|
42
|
+
"instrumentation.ts",
|
|
41
43
|
"next.config.mjs",
|
|
42
44
|
"tsconfig.json",
|
|
43
45
|
"next-env.d.ts",
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
"build": "next build",
|
|
52
54
|
"start": "rev4a serve",
|
|
53
55
|
"start:all": "rev4a serve",
|
|
56
|
+
"refresh:pricing": "node scripts/refresh-model-pricing.mjs",
|
|
54
57
|
"smoke:db-bootstrap": "node scripts/smoke-db-bootstrap.mjs",
|
|
55
58
|
"postinstall": "node bin/postinstall.js"
|
|
56
59
|
},
|
package/proxy.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
import { jwtVerify } from 'jose';
|
|
4
|
+
import { getJwtSecret } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
/* Public routes — no auth required */
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
const PUBLIC_PREFIXES = ['/login', '/setup'];
|
|
10
|
+
|
|
11
|
+
/* ------------------------------------------------------------------ */
|
|
12
|
+
/* Bypass — static assets and APIs have their own auth */
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
function isStaticOrApi(pathname: string): boolean {
|
|
15
|
+
return (
|
|
16
|
+
pathname.startsWith('/_next') ||
|
|
17
|
+
pathname.startsWith('/api') ||
|
|
18
|
+
pathname.startsWith('/favicon') ||
|
|
19
|
+
pathname.startsWith('/icon') ||
|
|
20
|
+
pathname.startsWith('/apple-touch') ||
|
|
21
|
+
pathname.startsWith('/rev4a-logo') ||
|
|
22
|
+
pathname.startsWith('/manifest') ||
|
|
23
|
+
pathname.includes('.') // file extensions (png, svg, ico, etc.)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* ------------------------------------------------------------------ */
|
|
28
|
+
/* Proxy (formerly middleware — Next.js 16 renamed the convention; */
|
|
29
|
+
/* runs on Node.js runtime by default now, so it shares the same */
|
|
30
|
+
/* env+file secret resolution as every API route instead of its own */
|
|
31
|
+
/* env-only copy.) */
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
export async function proxy(request: NextRequest) {
|
|
34
|
+
const { pathname } = request.nextUrl;
|
|
35
|
+
|
|
36
|
+
// Public routes
|
|
37
|
+
if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) {
|
|
38
|
+
return NextResponse.next();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Static assets & API routes
|
|
42
|
+
if (isStaticOrApi(pathname)) {
|
|
43
|
+
return NextResponse.next();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Protected route — verify JWT
|
|
47
|
+
const token = request.cookies.get('rev4a_token')?.value;
|
|
48
|
+
if (!token) {
|
|
49
|
+
const loginUrl = new URL('/login', request.url);
|
|
50
|
+
return NextResponse.redirect(loginUrl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const secret = new TextEncoder().encode(getJwtSecret());
|
|
55
|
+
await jwtVerify(token, secret);
|
|
56
|
+
return NextResponse.next();
|
|
57
|
+
} catch {
|
|
58
|
+
const loginUrl = new URL('/login', request.url);
|
|
59
|
+
return NextResponse.redirect(loginUrl);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ------------------------------------------------------------------ */
|
|
64
|
+
/* Matcher — only run on page routes, skip Next.js internals */
|
|
65
|
+
/* ------------------------------------------------------------------ */
|
|
66
|
+
export const config = {
|
|
67
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
68
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Refresh model-pricing.json and models.config.json from the live OpenRouter
|
|
4
|
+
* catalogue.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: both files are maintained by hand and drift. When this was
|
|
7
|
+
* written, 16 of 79 OpenRouter entries had stale prices — google/gemini-2.5-flash
|
|
8
|
+
* understated the real cost by ~8x. The dashboard reads these files, and so does
|
|
9
|
+
* daemon.js when it writes cost_usd rows, so a wrong number here is a wrong
|
|
10
|
+
* number in every spend figure the product reports.
|
|
11
|
+
*
|
|
12
|
+
* SCOPE — deliberately limited to `openrouter/*` models.
|
|
13
|
+
* OpenRouter resells models we also offer direct (openai, anthropic, google,
|
|
14
|
+
* deepseek, qwen). Their rates are NOT the same: at time of writing 8 of the
|
|
15
|
+
* 25 overlapping models differ — openai/gpt-5.6-sol is $5/$30 direct in our
|
|
16
|
+
* file but $2/$10 on OpenRouter. We can't tell from OpenRouter's catalogue
|
|
17
|
+
* what the direct vendor charges, so those entries are left alone. Keep them
|
|
18
|
+
* updated by hand from each provider's own pricing page.
|
|
19
|
+
*
|
|
20
|
+
* The endpoint is public — no API key, CDN-cached, never billed.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node scripts/refresh-model-pricing.mjs # apply
|
|
24
|
+
* node scripts/refresh-model-pricing.mjs --dry-run # preview only
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const PRICING_FILE = path.join(ROOT, 'model-pricing.json');
|
|
33
|
+
const MODELS_FILE = path.join(ROOT, 'models.config.json');
|
|
34
|
+
const CATALOGUE_URL = 'https://openrouter.ai/api/v1/models';
|
|
35
|
+
|
|
36
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
37
|
+
|
|
38
|
+
function perMillion(raw) {
|
|
39
|
+
if (raw === undefined || raw === null) return null;
|
|
40
|
+
const n = parseFloat(raw);
|
|
41
|
+
// -1 marks router models (OpenRouter Auto): dynamic, not a real price.
|
|
42
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
43
|
+
return n * 1_000_000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Round to the precision the file already uses, so we don't churn on float noise. */
|
|
47
|
+
function round(n) {
|
|
48
|
+
return Math.round(n * 1e6) / 1e6;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Serialise model-pricing.json preserving its `5.0` style for whole numbers.
|
|
53
|
+
* JSON.stringify writes `5`, which is the same value but rewrites ~114 lines
|
|
54
|
+
* that didn't change — burying the handful of real price moves in noise and
|
|
55
|
+
* making the diff unreviewable on every run.
|
|
56
|
+
*/
|
|
57
|
+
function serialisePricing(pricing) {
|
|
58
|
+
const num = (n) => (Number.isInteger(n) ? n.toFixed(1) : String(n));
|
|
59
|
+
const body = Object.entries(pricing)
|
|
60
|
+
.map(([id, p]) => ` ${JSON.stringify(id)}: {\n "input": ${num(p.input)},\n "output": ${num(p.output)}\n }`)
|
|
61
|
+
.join(',\n');
|
|
62
|
+
return `{\n${body}\n}\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Our catalogue namespaces OpenRouter entries as `openrouter/<upstream id>`,
|
|
67
|
+
* but OpenRouter also publishes house models under a literal `openrouter/`
|
|
68
|
+
* vendor prefix (openrouter/auto). Try the stripped form, then the id as-is.
|
|
69
|
+
*/
|
|
70
|
+
function upstreamCandidates(id) {
|
|
71
|
+
const stripped = id.slice('openrouter/'.length);
|
|
72
|
+
return stripped === id ? [id] : [stripped, id];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
console.log(`Fetching ${CATALOGUE_URL} …`);
|
|
77
|
+
const res = await fetch(CATALOGUE_URL, { headers: { accept: 'application/json' } });
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
console.error(`✗ Catalogue returned HTTP ${res.status}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const body = await res.json();
|
|
83
|
+
if (!Array.isArray(body?.data) || body.data.length === 0) {
|
|
84
|
+
console.error('✗ Unexpected catalogue shape');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const live = new Map(body.data.map((m) => [m.id, m]));
|
|
88
|
+
console.log(` ${live.size} models upstream\n`);
|
|
89
|
+
|
|
90
|
+
const modelsDoc = JSON.parse(fs.readFileSync(MODELS_FILE, 'utf-8'));
|
|
91
|
+
const models = Array.isArray(modelsDoc) ? modelsDoc : modelsDoc.models;
|
|
92
|
+
const pricing = JSON.parse(fs.readFileSync(PRICING_FILE, 'utf-8'));
|
|
93
|
+
|
|
94
|
+
const priceChanges = [];
|
|
95
|
+
const modalityChanges = [];
|
|
96
|
+
let missing = 0;
|
|
97
|
+
let dynamic = 0;
|
|
98
|
+
|
|
99
|
+
for (const m of models) {
|
|
100
|
+
if (m.provider !== 'openrouter') continue;
|
|
101
|
+
|
|
102
|
+
let up = null;
|
|
103
|
+
for (const candidate of upstreamCandidates(m.id)) {
|
|
104
|
+
if (live.has(candidate)) { up = live.get(candidate); break; }
|
|
105
|
+
}
|
|
106
|
+
if (!up) {
|
|
107
|
+
missing++;
|
|
108
|
+
console.log(` ? ${m.id} — not in the upstream catalogue, left as-is`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Pricing
|
|
113
|
+
const input = perMillion(up.pricing?.prompt);
|
|
114
|
+
const output = perMillion(up.pricing?.completion);
|
|
115
|
+
if (input === null || output === null) {
|
|
116
|
+
dynamic++; // router model — keep whatever the file says (-1/-1)
|
|
117
|
+
} else {
|
|
118
|
+
const current = pricing[m.id];
|
|
119
|
+
const next = { input: round(input), output: round(output) };
|
|
120
|
+
if (!current || current.input !== next.input || current.output !== next.output) {
|
|
121
|
+
priceChanges.push({
|
|
122
|
+
id: m.id,
|
|
123
|
+
from: current ? `$${current.input}/$${current.output}` : '(none)',
|
|
124
|
+
to: `$${next.input}/$${next.output}`,
|
|
125
|
+
});
|
|
126
|
+
pricing[m.id] = next;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Modality — updating the curated file (rather than overriding at runtime)
|
|
131
|
+
// keeps the UI and app/api/gateway/sync.ts, which provisions containers
|
|
132
|
+
// from this same value, in agreement.
|
|
133
|
+
const upModality = up.architecture?.modality;
|
|
134
|
+
if (upModality && upModality !== m.modality) {
|
|
135
|
+
modalityChanges.push({ id: m.id, from: m.modality ?? '(none)', to: upModality });
|
|
136
|
+
m.modality = upModality;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`\nPricing: ${priceChanges.length} changed`);
|
|
141
|
+
for (const c of priceChanges) console.log(` ${c.id}\n ${c.from} → ${c.to}`);
|
|
142
|
+
|
|
143
|
+
console.log(`\nModality: ${modalityChanges.length} changed`);
|
|
144
|
+
for (const c of modalityChanges) console.log(` ${c.id}\n ${c.from} → ${c.to}`);
|
|
145
|
+
|
|
146
|
+
const direct = models.filter((m) => m.provider !== 'openrouter').length;
|
|
147
|
+
console.log(
|
|
148
|
+
`\nSkipped: ${dynamic} dynamic (router), ${missing} not upstream, ` +
|
|
149
|
+
`${direct} direct-provider (different vendor, different rates — update by hand)`,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (priceChanges.length === 0 && modalityChanges.length === 0) {
|
|
153
|
+
console.log('\n✓ Already up to date.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
console.log('\n(dry run — nothing written)');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(PRICING_FILE, serialisePricing(pricing), 'utf-8');
|
|
163
|
+
fs.writeFileSync(MODELS_FILE, JSON.stringify(modelsDoc, null, 2) + '\n', 'utf-8');
|
|
164
|
+
console.log('\n✓ Wrote model-pricing.json and models.config.json');
|
|
165
|
+
console.log(' Review the diff before committing.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((e) => {
|
|
169
|
+
console.error(`✗ ${e.message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
package/lib/auth.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
-
import { jwtVerify } from 'jose';
|
|
3
|
-
import { TOKEN } from '@/lib/db';
|
|
4
|
-
|
|
5
|
-
const JWT_SECRET = new TextEncoder().encode(
|
|
6
|
-
process.env.REV4A_JWT_SECRET ?? 'rev4a-jwt-secret-change-in-prod',
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
export async function requireBrowserAuth(request: NextRequest): Promise<NextResponse | null> {
|
|
10
|
-
const token = new URL(request.url).searchParams.get('token');
|
|
11
|
-
const auth = request.headers.get('authorization');
|
|
12
|
-
|
|
13
|
-
if (token === TOKEN || auth === `Bearer ${TOKEN}`) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const cookieToken = request.cookies.get('rev4a_token')?.value;
|
|
18
|
-
if (cookieToken) {
|
|
19
|
-
try {
|
|
20
|
-
await jwtVerify(cookieToken, JWT_SECRET);
|
|
21
|
-
return null;
|
|
22
|
-
} catch {
|
|
23
|
-
// Fall through to unauthorized.
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
28
|
-
}
|
package/lib/requireAuth.tsx
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { cookies } from 'next/headers';
|
|
2
|
-
import { redirect } from 'next/navigation';
|
|
3
|
-
import { jwtVerify } from 'jose';
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
import { ENV_FILE } from './rev4a-paths';
|
|
6
|
-
|
|
7
|
-
function getJwtSecret(): Uint8Array {
|
|
8
|
-
const fromEnv = process.env.REV4A_JWT_SECRET;
|
|
9
|
-
if (typeof fromEnv === 'string' && fromEnv.length > 0) {
|
|
10
|
-
return new TextEncoder().encode(fromEnv);
|
|
11
|
-
}
|
|
12
|
-
// Fallback: read from persistent .env
|
|
13
|
-
try {
|
|
14
|
-
if (fs.existsSync(ENV_FILE)) {
|
|
15
|
-
const content = fs.readFileSync(ENV_FILE, 'utf-8');
|
|
16
|
-
const match = content.match(/^REV4A_JWT_SECRET=(.*)$/m);
|
|
17
|
-
if (match && match[1].trim()) {
|
|
18
|
-
return new TextEncoder().encode(match[1].trim());
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
} catch { /* ignore */ }
|
|
22
|
-
return new TextEncoder().encode('');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function requireAuth(): Promise<void> {
|
|
26
|
-
const cookieStore = await cookies();
|
|
27
|
-
const token = cookieStore.get('rev4a_token')?.value;
|
|
28
|
-
|
|
29
|
-
if (token) {
|
|
30
|
-
try {
|
|
31
|
-
await jwtVerify(token, getJwtSecret());
|
|
32
|
-
return;
|
|
33
|
-
} catch {
|
|
34
|
-
// Invalid — redirect
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
redirect('/login');
|
|
38
|
-
}
|