@flame0510/project-aether 1.1.9
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 +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
8
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
9
|
+
const { searchParams } = new URL(request.url);
|
|
10
|
+
const limit = Math.min(Number.parseInt(searchParams.get('limit') ?? '60', 10), 1000);
|
|
11
|
+
try {
|
|
12
|
+
const db = openDb();
|
|
13
|
+
const latest = db
|
|
14
|
+
.prepare(
|
|
15
|
+
`SELECT cpu_percent as cpu, ram_used_mb, ram_total_mb, disk_used_gb, disk_total_gb,
|
|
16
|
+
load_avg_1m as load_avg, ts FROM system_metrics ORDER BY ts DESC LIMIT 1`,
|
|
17
|
+
)
|
|
18
|
+
.get();
|
|
19
|
+
const history = db
|
|
20
|
+
.prepare(
|
|
21
|
+
'SELECT cpu_percent as cpu, ram_used_mb, ram_total_mb, ts FROM system_metrics ORDER BY ts DESC LIMIT ?',
|
|
22
|
+
)
|
|
23
|
+
.all(limit)
|
|
24
|
+
.reverse();
|
|
25
|
+
const stats_24h = db
|
|
26
|
+
.prepare(
|
|
27
|
+
'SELECT ROUND(AVG(cpu_percent)) as cpu_avg, MAX(cpu_percent) as cpu_max FROM system_metrics WHERE ts > ?',
|
|
28
|
+
)
|
|
29
|
+
.get(Math.floor(Date.now() / 1000) - 86400);
|
|
30
|
+
db.close();
|
|
31
|
+
return NextResponse.json({ latest, history, stats_24h });
|
|
32
|
+
} catch (e: unknown) {
|
|
33
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { MODELS_CONFIG_FILE } from '@/lib/rev4a-paths';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { readProviderKeys } from '@/app/api/gateway/provider/keys';
|
|
7
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
8
|
+
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
interface ModelsConfigEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
provider: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CONFIG_PATH = MODELS_CONFIG_FILE;
|
|
19
|
+
|
|
20
|
+
const PROVIDER_EMOJI: Record<string, string> = {
|
|
21
|
+
deepseek: '🧠',
|
|
22
|
+
openrouter: '🌐',
|
|
23
|
+
groq: '⚡',
|
|
24
|
+
openai: '🤖',
|
|
25
|
+
'openai-codex': '🤖',
|
|
26
|
+
anthropic: '🟣',
|
|
27
|
+
rev4a: '🔴',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PROVIDER_LABEL: Record<string, string> = {
|
|
31
|
+
deepseek: 'DeepSeek',
|
|
32
|
+
openrouter: 'OpenRouter',
|
|
33
|
+
groq: 'Groq',
|
|
34
|
+
openai: 'OpenAI',
|
|
35
|
+
'openai-codex': 'OpenAI Codex',
|
|
36
|
+
anthropic: 'Anthropic',
|
|
37
|
+
rev4a: 'Rev4a',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function loadModelsConfig(): ModelsConfigEntry[] {
|
|
41
|
+
try {
|
|
42
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
if (Array.isArray(parsed.models)) return parsed.models as ModelsConfigEntry[];
|
|
45
|
+
} catch {}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function GET(_req: NextRequest): Promise<NextResponse> {
|
|
50
|
+
const request = _req;
|
|
51
|
+
const allModels = loadModelsConfig();
|
|
52
|
+
const providerKeys = readProviderKeys();
|
|
53
|
+
const configured = new Set(Object.keys(providerKeys));
|
|
54
|
+
|
|
55
|
+
const eligible = allModels.filter((m) => {
|
|
56
|
+
if (!m.enabled) return false;
|
|
57
|
+
if (configured.has(m.provider)) return true;
|
|
58
|
+
// rev4a/* aliases resolve to deepseek upstream
|
|
59
|
+
if (m.provider === 'rev4a' && configured.has('deepseek')) return true;
|
|
60
|
+
return false;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const grouped = new Map<string, { provider: string; emoji: string; models: { id: string; label: string }[] }>();
|
|
64
|
+
|
|
65
|
+
for (const model of eligible) {
|
|
66
|
+
if (!grouped.has(model.provider)) {
|
|
67
|
+
grouped.set(model.provider, {
|
|
68
|
+
provider: PROVIDER_LABEL[model.provider] || model.provider,
|
|
69
|
+
emoji: PROVIDER_EMOJI[model.provider] || '🔌',
|
|
70
|
+
models: [],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
grouped.get(model.provider)!.models.push({ id: model.id, label: model.name });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return NextResponse.json(Array.from(grouped.values()));
|
|
77
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
const SAFE_PLUGIN_ID = /^[a-zA-Z0-9._:@/-]+$/;
|
|
6
|
+
|
|
7
|
+
export async function GET(request) {
|
|
8
|
+
const denied = await requireAuthJWT(request); if (denied) return denied;
|
|
9
|
+
try {
|
|
10
|
+
const raw = execSync('openclaw plugins list --json 2>/dev/null', {
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
timeout: 10000,
|
|
13
|
+
});
|
|
14
|
+
const data = JSON.parse(raw);
|
|
15
|
+
return NextResponse.json(data);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
return NextResponse.json({ error: err.message, plugins: [] }, { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function POST(request) {
|
|
22
|
+
const denied = await requireAuthJWT(request); if (denied) return denied;
|
|
23
|
+
try {
|
|
24
|
+
const { action, pluginId } = await request.json();
|
|
25
|
+
if (!pluginId || !['enable', 'disable'].includes(action)) {
|
|
26
|
+
return NextResponse.json({ error: 'Invalid action or pluginId' }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
if (typeof pluginId !== 'string' || !SAFE_PLUGIN_ID.test(pluginId)) {
|
|
29
|
+
return NextResponse.json({ error: 'Invalid pluginId' }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
const cmd = `openclaw plugins ${action} ${JSON.stringify(pluginId)} 2>&1`;
|
|
32
|
+
const output = execSync(cmd, { encoding: 'utf8', timeout: 15000 });
|
|
33
|
+
return NextResponse.json({ ok: true, output });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return NextResponse.json({ error: err.message }, { status: 500 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rev4a Provider Gateway — upstream logic
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for calling upstream AI providers.
|
|
5
|
+
* Used by both /api/provider/v1/chat/completions and /api/assistant
|
|
6
|
+
* to avoid HTTP loopback (calling localhost from inside Next.js).
|
|
7
|
+
*/
|
|
8
|
+
import { detectPublicHost } from '@/lib/docker-utils';
|
|
9
|
+
|
|
10
|
+
export const dynamic = 'force-dynamic';
|
|
11
|
+
|
|
12
|
+
export interface ProviderEnvConfig {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
authHeader: string;
|
|
15
|
+
/** Path prefix for chat completions (defaults to "/v1" if omitted). */
|
|
16
|
+
basePath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const PROVIDER_ENVS: Record<string, ProviderEnvConfig> = {
|
|
20
|
+
google: {
|
|
21
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
22
|
+
authHeader: 'x-goog-api-key',
|
|
23
|
+
},
|
|
24
|
+
openai: {
|
|
25
|
+
baseUrl: 'https://api.openai.com',
|
|
26
|
+
authHeader: 'Authorization',
|
|
27
|
+
},
|
|
28
|
+
anthropic: {
|
|
29
|
+
baseUrl: 'https://api.anthropic.com',
|
|
30
|
+
authHeader: 'x-api-key',
|
|
31
|
+
},
|
|
32
|
+
deepseek: {
|
|
33
|
+
baseUrl: 'https://api.deepseek.com',
|
|
34
|
+
authHeader: 'Authorization',
|
|
35
|
+
},
|
|
36
|
+
openrouter: {
|
|
37
|
+
baseUrl: 'https://openrouter.ai/api',
|
|
38
|
+
authHeader: 'Authorization',
|
|
39
|
+
},
|
|
40
|
+
groq: {
|
|
41
|
+
baseUrl: 'https://api.groq.com/openai',
|
|
42
|
+
authHeader: 'Authorization',
|
|
43
|
+
},
|
|
44
|
+
kimi: {
|
|
45
|
+
baseUrl: 'https://api.moonshot.cn',
|
|
46
|
+
authHeader: 'Authorization',
|
|
47
|
+
},
|
|
48
|
+
glm: {
|
|
49
|
+
baseUrl: 'https://api.z.ai',
|
|
50
|
+
basePath: '/api/paas/v4',
|
|
51
|
+
authHeader: 'Authorization',
|
|
52
|
+
},
|
|
53
|
+
qwen: {
|
|
54
|
+
baseUrl: 'https://dashscope.aliyuncs.com',
|
|
55
|
+
basePath: '/compatible-mode/v1',
|
|
56
|
+
authHeader: 'Authorization',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Map rev4a/ model aliases to upstream provider/model pairs. */
|
|
61
|
+
export const MODEL_ALIASES: Record<string, { provider: string; model: string }> = {
|
|
62
|
+
'rev4a/deepseek-v4-flash': { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
|
63
|
+
'rev4a/deepseek-v4-pro': { provider: 'deepseek', model: 'deepseek-v4-pro' },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface UpstreamRequest {
|
|
67
|
+
model?: string;
|
|
68
|
+
provider?: string;
|
|
69
|
+
messages?: { role: string; content: string }[];
|
|
70
|
+
stream?: boolean;
|
|
71
|
+
max_tokens?: number;
|
|
72
|
+
temperature?: number;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function extractProviderAndModel(request: UpstreamRequest): { provider: string; model: string } {
|
|
77
|
+
const modelStr = request.model || '';
|
|
78
|
+
if (request.provider) {
|
|
79
|
+
return { provider: request.provider, model: modelStr };
|
|
80
|
+
}
|
|
81
|
+
// Check aliases first (rev4a/deepseek-v4-flash -> deepseek/deepseek-v4-flash)
|
|
82
|
+
if (modelStr && MODEL_ALIASES[modelStr]) {
|
|
83
|
+
return { ...MODEL_ALIASES[modelStr] };
|
|
84
|
+
}
|
|
85
|
+
const parts = modelStr.split('/');
|
|
86
|
+
if (parts.length >= 2 && PROVIDER_ENVS[parts[0]]) {
|
|
87
|
+
return { provider: parts[0], model: parts.slice(1).join('/') };
|
|
88
|
+
}
|
|
89
|
+
// Default fallback: assume the model is a raw upstream model ID
|
|
90
|
+
if (parts.length >= 3 && PROVIDER_ENVS[parts[1]]) {
|
|
91
|
+
return { provider: parts[1], model: parts.slice(2).join('/') };
|
|
92
|
+
}
|
|
93
|
+
return { provider: 'deepseek', model: modelStr || 'deepseek-chat' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getApiKey(provider: string, storedKeys: Record<string, string>): string | null {
|
|
97
|
+
return storedKeys[provider] || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function callUpstream(
|
|
101
|
+
provider: string,
|
|
102
|
+
model: string,
|
|
103
|
+
body: UpstreamRequest,
|
|
104
|
+
apiKey: string,
|
|
105
|
+
): Promise<Response> {
|
|
106
|
+
const envConfig = PROVIDER_ENVS[provider];
|
|
107
|
+
if (!envConfig) {
|
|
108
|
+
return new Response(
|
|
109
|
+
JSON.stringify({ error: { message: `Unknown provider: ${provider}`, type: 'invalid_request_error' } }),
|
|
110
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const basePath = envConfig.basePath || '/v1';
|
|
115
|
+
const targetUrl = `${envConfig.baseUrl}${basePath}/chat/completions`;
|
|
116
|
+
|
|
117
|
+
const upstreamBody: Record<string, unknown> = { ...body, model: model || undefined };
|
|
118
|
+
delete upstreamBody.provider;
|
|
119
|
+
|
|
120
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
121
|
+
|
|
122
|
+
if (envConfig.authHeader === 'x-api-key') {
|
|
123
|
+
headers['x-api-key'] = apiKey;
|
|
124
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
125
|
+
} else {
|
|
126
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (provider === 'openrouter') {
|
|
130
|
+
headers['HTTP-Referer'] = `http://${detectPublicHost()}:3740`;
|
|
131
|
+
headers['X-Title'] = 'Rev4a Gateway';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Force identity encoding to avoid gzip-related HTTP/2 truncation issues via Traefik
|
|
135
|
+
headers['Accept-Encoding'] = 'identity';
|
|
136
|
+
|
|
137
|
+
return fetch(targetUrl, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers,
|
|
140
|
+
body: JSON.stringify(upstreamBody),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rev4a Provider Gateway — v1
|
|
3
|
+
*
|
|
4
|
+
* OpenAI-compatible chat completions endpoint.
|
|
5
|
+
* POST /api/provider/v1/chat/completions
|
|
6
|
+
*
|
|
7
|
+
* Authentication uses provider keys stored in data/provider-keys.json.
|
|
8
|
+
* The Authorization Bearer token must match a configured provider key.
|
|
9
|
+
* The rev4a master key (REV4A_API_KEY env var) unlocks all providers.
|
|
10
|
+
*/
|
|
11
|
+
import { NextRequest } from 'next/server';
|
|
12
|
+
import { readProviderKeys } from '@/app/api/gateway/provider/keys';
|
|
13
|
+
import {
|
|
14
|
+
extractProviderAndModel,
|
|
15
|
+
getApiKey,
|
|
16
|
+
callUpstream,
|
|
17
|
+
} from '@/app/api/provider/upstream';
|
|
18
|
+
|
|
19
|
+
export const dynamic = 'force-dynamic';
|
|
20
|
+
export const runtime = 'nodejs';
|
|
21
|
+
|
|
22
|
+
interface ProviderV1Request {
|
|
23
|
+
model?: string;
|
|
24
|
+
provider?: string;
|
|
25
|
+
messages?: { role: string; content: string }[];
|
|
26
|
+
stream?: boolean;
|
|
27
|
+
max_tokens?: number;
|
|
28
|
+
temperature?: number;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Serialize to JSON and return a Response with explicit Content-Length to avoid Traefik HTTP/2 issues. */
|
|
33
|
+
function jsonResponse(data: unknown, status = 200, extraHeaders?: Record<string, string>): Response {
|
|
34
|
+
const body = JSON.stringify(data);
|
|
35
|
+
const byteLen = Buffer.byteLength(body, 'utf-8');
|
|
36
|
+
const headers = new Headers({
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
'Content-Length': String(byteLen),
|
|
39
|
+
'Cache-Control': 'no-store, max-age=0, must-revalidate',
|
|
40
|
+
...extraHeaders,
|
|
41
|
+
});
|
|
42
|
+
return new Response(body, { status, headers });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function errorResponse(message: string, type: string, status: number): Response {
|
|
46
|
+
return jsonResponse({ error: { message, type } }, status);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function POST(request: NextRequest) {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Resolve provider from Authorization token
|
|
54
|
+
const authHeader = request.headers.get('Authorization') || '';
|
|
55
|
+
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
56
|
+
const providerKeys = readProviderKeys();
|
|
57
|
+
|
|
58
|
+
// Check if token matches any stored key
|
|
59
|
+
const isAuthenticated = Object.values(providerKeys).some((key) => token === key);
|
|
60
|
+
|
|
61
|
+
// Also accept the rev4a master API key env var
|
|
62
|
+
const rev4aMasterKey = process.env.REV4A_API_KEY;
|
|
63
|
+
const isMasterAuth = !!rev4aMasterKey && token === rev4aMasterKey;
|
|
64
|
+
|
|
65
|
+
if (!isAuthenticated && !isMasterAuth) {
|
|
66
|
+
return errorResponse(
|
|
67
|
+
'Invalid API key. Use your Rev4a API key from the Gateway page via Authorization: Bearer <key>.',
|
|
68
|
+
'authentication_error',
|
|
69
|
+
401,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const body: ProviderV1Request = await request.json();
|
|
74
|
+
const { provider, model } = extractProviderAndModel(body);
|
|
75
|
+
|
|
76
|
+
const apiKey = getApiKey(provider, providerKeys);
|
|
77
|
+
if (!apiKey) {
|
|
78
|
+
return errorResponse(
|
|
79
|
+
`API key not configured for provider '${provider}'. Add it from the Gateway page.`,
|
|
80
|
+
'configuration_error',
|
|
81
|
+
401,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const upstreamResp = await callUpstream(provider, model, body, apiKey);
|
|
86
|
+
const latency = Date.now() - start;
|
|
87
|
+
|
|
88
|
+
// Streaming passthrough
|
|
89
|
+
if (upstreamResp.headers.get('content-type')?.includes('text/event-stream') || body.stream) {
|
|
90
|
+
const responseHeaders = new Headers();
|
|
91
|
+
upstreamResp.headers.forEach((value, key) => {
|
|
92
|
+
const forbidden = ['transfer-encoding', 'connection', 'keep-alive', 'content-length'];
|
|
93
|
+
if (!forbidden.includes(key.toLowerCase())) {
|
|
94
|
+
responseHeaders.set(key, value);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
responseHeaders.set('x-rev4a-gateway', 'true');
|
|
98
|
+
responseHeaders.set('x-rev4a-provider', provider);
|
|
99
|
+
responseHeaders.set('x-rev4a-latency-ms', String(latency));
|
|
100
|
+
return new Response(upstreamResp.body, {
|
|
101
|
+
status: upstreamResp.status,
|
|
102
|
+
statusText: upstreamResp.statusText,
|
|
103
|
+
headers: responseHeaders,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Non-streaming: parse upstream JSON
|
|
108
|
+
const responseText = await upstreamResp.text();
|
|
109
|
+
let responseJson: Record<string, unknown> = {};
|
|
110
|
+
try {
|
|
111
|
+
responseJson = JSON.parse(responseText);
|
|
112
|
+
} catch {
|
|
113
|
+
responseJson = { raw: responseText };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build with explicit Content-Length (in bytes, not chars)
|
|
117
|
+
const finalBodyStr = JSON.stringify(
|
|
118
|
+
{ ...responseJson, _gateway: { provider, latencyMs: latency, timestamp: Date.now() } },
|
|
119
|
+
);
|
|
120
|
+
const byteLen = Buffer.byteLength(finalBodyStr, 'utf-8');
|
|
121
|
+
|
|
122
|
+
const responseHeaders = new Headers({
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'Content-Length': String(byteLen),
|
|
125
|
+
'Cache-Control': 'no-store, max-age=0, must-revalidate',
|
|
126
|
+
});
|
|
127
|
+
upstreamResp.headers.forEach((value, key) => {
|
|
128
|
+
const forbidden = ['transfer-encoding', 'connection', 'keep-alive', 'content-length'];
|
|
129
|
+
if (!forbidden.includes(key.toLowerCase())) {
|
|
130
|
+
responseHeaders.set(key, value);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
responseHeaders.set('x-rev4a-gateway', 'true');
|
|
134
|
+
responseHeaders.set('x-rev4a-provider', provider);
|
|
135
|
+
responseHeaders.set('x-rev4a-latency-ms', String(latency));
|
|
136
|
+
|
|
137
|
+
return new Response(finalBodyStr, {
|
|
138
|
+
status: upstreamResp.status,
|
|
139
|
+
headers: responseHeaders,
|
|
140
|
+
});
|
|
141
|
+
} catch (err: unknown) {
|
|
142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
143
|
+
console.error('[provider/v1] Error:', message);
|
|
144
|
+
return errorResponse(`Gateway error: ${message}`, 'gateway_error', 502);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rev4a Provider Gateway — v1 Models List
|
|
3
|
+
*
|
|
4
|
+
* GET /api/provider/v1/models
|
|
5
|
+
*
|
|
6
|
+
* Returns models available through the Rev4a Gateway
|
|
7
|
+
* in OpenAI-compatible format, filtered by the provider
|
|
8
|
+
* identified from the Authorization token.
|
|
9
|
+
*
|
|
10
|
+
* The model list is read from models.config.json in the project root.
|
|
11
|
+
* If the file doesn't exist, falls back to provider env config + static defaults.
|
|
12
|
+
*/
|
|
13
|
+
import { NextResponse, NextRequest } from 'next/server';
|
|
14
|
+
import { MODELS_CONFIG_FILE } from '@/lib/rev4a-paths';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { readProviderKeys } from '@/app/api/gateway/provider/keys';
|
|
18
|
+
|
|
19
|
+
export const dynamic = 'force-dynamic';
|
|
20
|
+
export const runtime = 'nodejs';
|
|
21
|
+
|
|
22
|
+
interface ModelEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
object: string;
|
|
25
|
+
created: number;
|
|
26
|
+
owned_by: string;
|
|
27
|
+
permission: string[];
|
|
28
|
+
root: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ModelsConfigEntry {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
provider: string;
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CONFIG_PATH = MODELS_CONFIG_FILE;
|
|
39
|
+
|
|
40
|
+
/** Default model catalogue (known models for each provider) */
|
|
41
|
+
const DEFAULT_CATALOGUE: ModelsConfigEntry[] = [
|
|
42
|
+
// OpenAI
|
|
43
|
+
{ id: 'openai/gpt-5.6-sol', name: 'GPT-5.6 Sol', provider: 'openai', enabled: true },
|
|
44
|
+
{ id: 'openai/gpt-5.6-terra', name: 'GPT-5.6 Terra', provider: 'openai', enabled: true },
|
|
45
|
+
{ id: 'openai/gpt-5.6-luna', name: 'GPT-5.6 Luna', provider: 'openai', enabled: true },
|
|
46
|
+
{ id: 'openai/gpt-5.5', name: 'GPT-5.5', provider: 'openai', enabled: true },
|
|
47
|
+
{ id: 'openai/gpt-5.4', name: 'GPT-5.4', provider: 'openai', enabled: true },
|
|
48
|
+
{ id: 'openai/gpt-5.4-mini', name: 'GPT-5.4 Mini', provider: 'openai', enabled: true },
|
|
49
|
+
// Anthropic
|
|
50
|
+
{ id: 'anthropic/claude-fable-5', name: 'Claude Fable 5', provider: 'anthropic', enabled: false },
|
|
51
|
+
{ id: 'anthropic/claude-opus-5', name: 'Claude Opus 5', provider: 'anthropic', enabled: false },
|
|
52
|
+
{ id: 'anthropic/claude-sonnet-5', name: 'Claude Sonnet 5', provider: 'anthropic', enabled: false },
|
|
53
|
+
{ id: 'anthropic/claude-haiku-4-5', name: 'Claude Haiku 4.5', provider: 'anthropic', enabled: false },
|
|
54
|
+
// DeepSeek
|
|
55
|
+
{ id: 'deepseek/deepseek-v4-pro', name: 'DeepSeek V4 Pro', provider: 'deepseek', enabled: true },
|
|
56
|
+
{ id: 'deepseek/deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'deepseek', enabled: true },
|
|
57
|
+
// OpenRouter
|
|
58
|
+
{ id: 'openrouter/auto', name: 'OpenRouter Auto', provider: 'openrouter', enabled: true },
|
|
59
|
+
{ id: 'openrouter/deepseek/deepseek-v4-flash', name: 'DeepSeek V4 Flash (OR)', provider: 'openrouter', enabled: true },
|
|
60
|
+
{ id: 'openrouter/google/gemini-2.5-flash', name: 'Gemini 2.5 Flash (OR)', provider: 'openrouter', enabled: true },
|
|
61
|
+
{ id: 'openrouter/google/gemini-2.5-pro', name: 'Gemini 2.5 Pro (OR)', provider: 'openrouter', enabled: true },
|
|
62
|
+
{ id: 'openrouter/anthropic/claude-sonnet-4.6', name: 'Claude Sonnet 4.6 (OR)', provider: 'openrouter', enabled: false },
|
|
63
|
+
{ id: 'openrouter/deepseek/deepseek-r1-0528', name: 'DeepSeek R1 0528 (OR)', provider: 'openrouter', enabled: false },
|
|
64
|
+
// Groq
|
|
65
|
+
{ id: 'groq/llama-3.1-8b-instant', name: 'Llama 3.1 8B (Groq)', provider: 'groq', enabled: false },
|
|
66
|
+
{ id: 'groq/llama-3.3-70b-versatile', name: 'Llama 3.3 70B (Groq)', provider: 'groq', enabled: false },
|
|
67
|
+
// Kimi
|
|
68
|
+
{ id: 'kimi/kimi-k3', name: 'Kimi K3', provider: 'kimi', enabled: true },
|
|
69
|
+
{ id: 'kimi/kimi-k2.7-code', name: 'Kimi K2.7 Code', provider: 'kimi', enabled: false },
|
|
70
|
+
{ id: 'kimi/kimi-k2.7-code-highspeed', name: 'Kimi K2.7 Code HighSpeed', provider: 'kimi', enabled: false },
|
|
71
|
+
{ id: 'kimi/kimi-k2.6', name: 'Kimi K2.6', provider: 'kimi', enabled: false },
|
|
72
|
+
// GLM
|
|
73
|
+
{ id: 'glm/glm-5.2', name: 'GLM-5.2', provider: 'glm', enabled: false },
|
|
74
|
+
{ id: 'glm/glm-5.1', name: 'GLM-5.1', provider: 'glm', enabled: false },
|
|
75
|
+
{ id: 'glm/glm-5', name: 'GLM-5', provider: 'glm', enabled: false },
|
|
76
|
+
{ id: 'glm/glm-4.7', name: 'GLM-4.7', provider: 'glm', enabled: false },
|
|
77
|
+
{ id: 'glm/glm-4.7-flash', name: 'GLM-4.7 Flash', provider: 'glm', enabled: false },
|
|
78
|
+
{ id: 'glm/glm-4.7-flashx', name: 'GLM-4.7 FlashX', provider: 'glm', enabled: false },
|
|
79
|
+
{ id: 'glm/glm-5v-turbo', name: 'GLM-5V Turbo', provider: 'glm', enabled: false },
|
|
80
|
+
{ id: 'glm/glm-4.6v-flash', name: 'GLM-4.6V Flash', provider: 'glm', enabled: false },
|
|
81
|
+
// Qwen
|
|
82
|
+
{ id: 'qwen/qwen3.7-max', name: 'Qwen 3.7 Max', provider: 'qwen', enabled: false },
|
|
83
|
+
{ id: 'qwen/qwen3.7-plus', name: 'Qwen 3.7 Plus', provider: 'qwen', enabled: true },
|
|
84
|
+
{ id: 'qwen/qwen3.7-flash', name: 'Qwen 3.7 Flash', provider: 'qwen', enabled: false },
|
|
85
|
+
{ id: 'qwen/qwen3-coder-plus', name: 'Qwen 3 Coder Plus', provider: 'qwen', enabled: false },
|
|
86
|
+
{ id: 'qwen/qwen-long', name: 'Qwen Long', provider: 'qwen', enabled: false },
|
|
87
|
+
// Rev4a aliases (convenience models for the Rev4a proxy)
|
|
88
|
+
{ id: 'rev4a/deepseek-v4-flash', name: 'DeepSeek V4 Flash (Rev4a)', provider: 'rev4a', enabled: true },
|
|
89
|
+
{ id: 'rev4a/deepseek-v4-pro', name: 'DeepSeek V4 Pro (Rev4a)', provider: 'rev4a', enabled: true },
|
|
90
|
+
{ id: 'rev4a/fast', name: 'Rev4a Fast', provider: 'rev4a', enabled: false },
|
|
91
|
+
{ id: 'rev4a/code', name: 'Rev4a Code', provider: 'rev4a', enabled: false },
|
|
92
|
+
{ id: 'rev4a/reason', name: 'Rev4a Reason', provider: 'rev4a', enabled: false },
|
|
93
|
+
{ id: 'rev4a/fallback-general', name: 'Rev4a Fallback General', provider: 'rev4a', enabled: false },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/** Load the models config from file, or create from defaults */
|
|
97
|
+
function loadConfig(): ModelsConfigEntry[] {
|
|
98
|
+
try {
|
|
99
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (Array.isArray(parsed.models)) {
|
|
102
|
+
return parsed.models as ModelsConfigEntry[];
|
|
103
|
+
}
|
|
104
|
+
return DEFAULT_CATALOGUE;
|
|
105
|
+
} catch {
|
|
106
|
+
// File doesn't exist or invalid — write defaults
|
|
107
|
+
try {
|
|
108
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify({ models: DEFAULT_CATALOGUE }, null, 2), 'utf-8');
|
|
109
|
+
} catch {
|
|
110
|
+
// Can't write, fall back to defaults in memory
|
|
111
|
+
}
|
|
112
|
+
return DEFAULT_CATALOGUE;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Resolve the provider from the Authorization token by matching against provider-keys.json */
|
|
117
|
+
function resolveProviderFromToken(authHeader: string): string | null {
|
|
118
|
+
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
119
|
+
if (!token) return null;
|
|
120
|
+
|
|
121
|
+
const providerKeys = readProviderKeys();
|
|
122
|
+
|
|
123
|
+
// Match the token value against stored provider keys
|
|
124
|
+
for (const [, key] of Object.entries(providerKeys)) {
|
|
125
|
+
if (token === key) return 'authenticated';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Also try matching the rev4a API key env var (legacy compat)
|
|
129
|
+
if (token === process.env.REV4A_API_KEY) return 'all';
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function GET(request: NextRequest) {
|
|
135
|
+
const allModels = loadConfig();
|
|
136
|
+
|
|
137
|
+
// Determine provider from Authorization token
|
|
138
|
+
const authHeader = request.headers.get('Authorization') || '';
|
|
139
|
+
const provider = resolveProviderFromToken(authHeader);
|
|
140
|
+
|
|
141
|
+
if (!provider) {
|
|
142
|
+
// No valid token — return empty list to avoid leaking model info
|
|
143
|
+
return NextResponse.json({
|
|
144
|
+
object: 'list',
|
|
145
|
+
total: 0,
|
|
146
|
+
data: [],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let activeModels: ModelsConfigEntry[];
|
|
151
|
+
|
|
152
|
+
// Token authenticated — return all enabled models
|
|
153
|
+
activeModels = allModels.filter((m) => m.enabled);
|
|
154
|
+
|
|
155
|
+
return NextResponse.json({
|
|
156
|
+
object: 'list',
|
|
157
|
+
total: activeModels.length,
|
|
158
|
+
data: activeModels.map((m) => ({
|
|
159
|
+
id: m.id,
|
|
160
|
+
object: 'model',
|
|
161
|
+
created: 1700000000,
|
|
162
|
+
owned_by: m.provider,
|
|
163
|
+
permission: [],
|
|
164
|
+
root: m.id,
|
|
165
|
+
})),
|
|
166
|
+
});
|
|
167
|
+
}
|