@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,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Provider API
|
|
3
|
+
*
|
|
4
|
+
* GET /api/gateway/provider — list providers and their model config
|
|
5
|
+
* POST /api/gateway/provider — set/remove API key + sync models + sync agent
|
|
6
|
+
* PUT /api/gateway/provider — toggle individual model on/off + sync
|
|
7
|
+
*/
|
|
8
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { readProviderKeys, writeProviderKeys } from './keys';
|
|
11
|
+
import { syncAllAgents } from '../sync';
|
|
12
|
+
import { detectProviderGatewayUrl } from '@/lib/docker-utils';
|
|
13
|
+
import { getPricing } from '@/lib/model-pricing';
|
|
14
|
+
import { loadModelsConfig, toggleModelOverride, type ModelConfigEntry } from '@/lib/model-catalogue';
|
|
15
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
16
|
+
|
|
17
|
+
export const dynamic = 'force-dynamic';
|
|
18
|
+
export const runtime = 'nodejs';
|
|
19
|
+
|
|
20
|
+
/* ------------------------------------------------------------------ */
|
|
21
|
+
/* Types */
|
|
22
|
+
/* ------------------------------------------------------------------ */
|
|
23
|
+
|
|
24
|
+
interface ProviderEnvConfig {
|
|
25
|
+
envKey: string;
|
|
26
|
+
label: string;
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
authHeader: string;
|
|
29
|
+
docsUrl: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PROVIDER_CONFIGS: Record<string, ProviderEnvConfig> = {
|
|
33
|
+
google: {
|
|
34
|
+
envKey: 'PROVIDER_GOOGLE_API_KEY',
|
|
35
|
+
label: 'Google Gemini',
|
|
36
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
37
|
+
authHeader: 'x-goog-api-key',
|
|
38
|
+
docsUrl: 'https://aistudio.google.com/app/apikey',
|
|
39
|
+
},
|
|
40
|
+
deepseek: {
|
|
41
|
+
envKey: 'PROVIDER_DEEPSEEK_API_KEY',
|
|
42
|
+
label: 'DeepSeek',
|
|
43
|
+
baseUrl: 'https://api.deepseek.com',
|
|
44
|
+
authHeader: 'Authorization',
|
|
45
|
+
docsUrl: 'https://platform.deepseek.com/api_keys',
|
|
46
|
+
},
|
|
47
|
+
openrouter: {
|
|
48
|
+
envKey: 'PROVIDER_OPENROUTER_API_KEY',
|
|
49
|
+
label: 'OpenRouter',
|
|
50
|
+
baseUrl: 'https://openrouter.ai/api',
|
|
51
|
+
authHeader: 'Authorization',
|
|
52
|
+
docsUrl: 'https://openrouter.ai/keys',
|
|
53
|
+
},
|
|
54
|
+
openai: {
|
|
55
|
+
envKey: 'PROVIDER_OPENAI_API_KEY',
|
|
56
|
+
label: 'OpenAI',
|
|
57
|
+
baseUrl: 'https://api.openai.com',
|
|
58
|
+
authHeader: 'Authorization',
|
|
59
|
+
docsUrl: 'https://platform.openai.com/api-keys',
|
|
60
|
+
},
|
|
61
|
+
anthropic: {
|
|
62
|
+
envKey: 'PROVIDER_ANTHROPIC_API_KEY',
|
|
63
|
+
label: 'Anthropic',
|
|
64
|
+
baseUrl: 'https://api.anthropic.com',
|
|
65
|
+
authHeader: 'x-api-key',
|
|
66
|
+
docsUrl: 'https://console.anthropic.com/settings/keys',
|
|
67
|
+
},
|
|
68
|
+
groq: {
|
|
69
|
+
envKey: 'PROVIDER_GROQ_API_KEY',
|
|
70
|
+
label: 'Groq',
|
|
71
|
+
baseUrl: 'https://api.groq.com/openai',
|
|
72
|
+
authHeader: 'Authorization',
|
|
73
|
+
docsUrl: 'https://console.groq.com/keys',
|
|
74
|
+
},
|
|
75
|
+
kimi: {
|
|
76
|
+
envKey: 'PROVIDER_KIMI_API_KEY',
|
|
77
|
+
label: 'Kimi',
|
|
78
|
+
baseUrl: 'https://api.moonshot.cn',
|
|
79
|
+
authHeader: 'Authorization',
|
|
80
|
+
docsUrl: 'https://platform.kimi.com/console/api-keys',
|
|
81
|
+
},
|
|
82
|
+
glm: {
|
|
83
|
+
envKey: 'PROVIDER_GLM_API_KEY',
|
|
84
|
+
label: 'GLM (Z.AI)',
|
|
85
|
+
baseUrl: 'https://api.z.ai',
|
|
86
|
+
authHeader: 'Authorization',
|
|
87
|
+
docsUrl: 'https://z.ai/manage-apikey/apikey-list',
|
|
88
|
+
},
|
|
89
|
+
qwen: {
|
|
90
|
+
envKey: 'PROVIDER_QWEN_API_KEY',
|
|
91
|
+
label: 'Qwen',
|
|
92
|
+
baseUrl: 'https://dashscope.aliyuncs.com',
|
|
93
|
+
authHeader: 'Authorization',
|
|
94
|
+
docsUrl: 'https://bailian.console.aliyun.com/cn-beijing?tab=model#/api-key',
|
|
95
|
+
},
|
|
96
|
+
rev4a: {
|
|
97
|
+
envKey: 'REV4A_API_KEY',
|
|
98
|
+
label: 'REV4A API KEY',
|
|
99
|
+
baseUrl: detectProviderGatewayUrl(),
|
|
100
|
+
authHeader: 'Authorization',
|
|
101
|
+
docsUrl: '',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/* ------------------------------------------------------------------ */
|
|
106
|
+
/* Helpers */
|
|
107
|
+
/* ------------------------------------------------------------------ */
|
|
108
|
+
|
|
109
|
+
function getProviderApiKey(providerName: string): string | null {
|
|
110
|
+
const keys = readProviderKeys();
|
|
111
|
+
return keys[providerName] || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function setProviderApiKey(providerName: string, value: string | null): void {
|
|
115
|
+
const keys = readProviderKeys();
|
|
116
|
+
if (value === null) {
|
|
117
|
+
delete keys[providerName];
|
|
118
|
+
} else {
|
|
119
|
+
keys[providerName] = value;
|
|
120
|
+
}
|
|
121
|
+
writeProviderKeys(keys);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function restartService(): { stdout: string; stderr: string } {
|
|
125
|
+
try {
|
|
126
|
+
const stdout = execSync('sudo systemctl restart rev4a.service 2>&1', { timeout: 15000, maxBuffer: 64 * 1024 }).toString();
|
|
127
|
+
return { stdout, stderr: '' };
|
|
128
|
+
} catch (e: unknown) {
|
|
129
|
+
const err = e as { stdout?: string; stderr?: string; message?: string };
|
|
130
|
+
return { stdout: err.stdout?.toString() || '', stderr: err.stderr?.toString() || err.message || 'Unknown error' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ------------------------------------------------------------------ */
|
|
135
|
+
/* Routes */
|
|
136
|
+
/* ------------------------------------------------------------------ */
|
|
137
|
+
|
|
138
|
+
export async function GET(request: NextRequest) {
|
|
139
|
+
const allModels = loadModelsConfig();
|
|
140
|
+
const providerKeys = readProviderKeys();
|
|
141
|
+
const providers = Object.entries(PROVIDER_CONFIGS).map(([key, cfg]) => {
|
|
142
|
+
const providerModels = allModels.filter((m) => m.provider === key);
|
|
143
|
+
return {
|
|
144
|
+
provider: key, label: cfg.label, configured: !!providerKeys[key],
|
|
145
|
+
baseUrl: cfg.baseUrl, docsUrl: cfg.docsUrl,
|
|
146
|
+
models: providerModels.map((m) => {
|
|
147
|
+
const entry: Record<string, unknown> = { id: m.id, name: m.name, enabled: m.enabled };
|
|
148
|
+
const p = getPricing(m.id);
|
|
149
|
+
if (p) entry.pricing = p;
|
|
150
|
+
if (m.modality) entry.modality = m.modality;
|
|
151
|
+
return entry;
|
|
152
|
+
}),
|
|
153
|
+
...(providerKeys[key] ? { apiKey: providerKeys[key] } : {}),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
return NextResponse.json({ providers });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function POST(request: NextRequest) {
|
|
160
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
161
|
+
try {
|
|
162
|
+
const body: { provider?: string; apiKey?: string; _syncOnly?: boolean } = await request.json();
|
|
163
|
+
|
|
164
|
+
if (body._syncOnly) {
|
|
165
|
+
const syncResults = syncAllAgents();
|
|
166
|
+
return NextResponse.json({ status: 'ok', sync: syncResults });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const providerName = body.provider;
|
|
170
|
+
if (!providerName || !PROVIDER_CONFIGS[providerName]) {
|
|
171
|
+
return NextResponse.json({ status: 'error', error: `Unknown provider: ${providerName}. Known: ${Object.keys(PROVIDER_CONFIGS).join(', ')}` }, { status: 400 });
|
|
172
|
+
}
|
|
173
|
+
const apiKeyValue = body.apiKey;
|
|
174
|
+
const isRemove = !apiKeyValue || apiKeyValue.trim() === '';
|
|
175
|
+
setProviderApiKey(providerName, isRemove ? null : apiKeyValue!.trim());
|
|
176
|
+
let syncResults: string[] = [];
|
|
177
|
+
try {
|
|
178
|
+
syncResults = syncAllAgents();
|
|
179
|
+
} catch (se) {
|
|
180
|
+
syncResults = [`Sync error: ${se instanceof Error ? se.message : String(se)}`];
|
|
181
|
+
}
|
|
182
|
+
return NextResponse.json({
|
|
183
|
+
status: 'ok', provider: providerName, configured: !isRemove,
|
|
184
|
+
sync: syncResults,
|
|
185
|
+
});
|
|
186
|
+
} catch (e: unknown) {
|
|
187
|
+
return NextResponse.json({ status: 'error', error: e instanceof Error ? e.message : String(e) }, { status: 500 });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function PUT(request: NextRequest) {
|
|
192
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
193
|
+
try {
|
|
194
|
+
const body: { modelId?: string; enabled?: boolean } = await request.json();
|
|
195
|
+
const { modelId, enabled } = body;
|
|
196
|
+
if (!modelId) return NextResponse.json({ status: 'error', error: 'modelId is required' }, { status: 400 });
|
|
197
|
+
if (typeof enabled !== 'boolean') return NextResponse.json({ status: 'error', error: 'enabled (boolean) is required' }, { status: 400 });
|
|
198
|
+
|
|
199
|
+
const models = loadModelsConfig();
|
|
200
|
+
const idx = models.findIndex((m) => m.id === modelId);
|
|
201
|
+
if (idx === -1) return NextResponse.json({ status: 'error', error: `Model not found: ${modelId}` }, { status: 404 });
|
|
202
|
+
|
|
203
|
+
toggleModelOverride(modelId, enabled);
|
|
204
|
+
const syncResults = syncAllAgents();
|
|
205
|
+
return NextResponse.json({ status: 'ok', modelId, enabled, provider: models[idx].provider, sync: syncResults });
|
|
206
|
+
} catch (e: unknown) {
|
|
207
|
+
return NextResponse.json({ status: 'error', error: e instanceof Error ? e.message : String(e) }, { status: 500 });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* Gateway Status API
|
|
4
|
+
*
|
|
5
|
+
* Aggregates live model/provider state from Rev4a agent containers.
|
|
6
|
+
* Only queries containers with AGENT_ID label — no hardcoded container names.
|
|
7
|
+
*
|
|
8
|
+
* GET /api/gateway
|
|
9
|
+
*/
|
|
10
|
+
import { NextResponse } from 'next/server';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
13
|
+
|
|
14
|
+
export const dynamic = 'force-dynamic';
|
|
15
|
+
|
|
16
|
+
interface GatewayModel {
|
|
17
|
+
key: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
model: string;
|
|
20
|
+
available: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GatewayProvider {
|
|
24
|
+
provider: string;
|
|
25
|
+
kind: string;
|
|
26
|
+
detail: string;
|
|
27
|
+
profiles: number;
|
|
28
|
+
labels: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface GatewayAgentStatus {
|
|
32
|
+
agentId: string;
|
|
33
|
+
containerName: string;
|
|
34
|
+
state: string;
|
|
35
|
+
defaultModel: string;
|
|
36
|
+
fallbacks: string[];
|
|
37
|
+
allowed: string[];
|
|
38
|
+
aliases: Record<string, string>;
|
|
39
|
+
providers: GatewayProvider[];
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ApiKeyStatus {
|
|
44
|
+
provider: string;
|
|
45
|
+
configured: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Run a command inside a Docker container */
|
|
49
|
+
function dockerExec(container: string, cmd: string, timeout = 15000): string {
|
|
50
|
+
try {
|
|
51
|
+
return execSync(
|
|
52
|
+
`docker exec ${container} sh -c ${JSON.stringify(cmd)}`,
|
|
53
|
+
{ timeout, maxBuffer: 1024 * 1024, encoding: 'utf-8' },
|
|
54
|
+
);
|
|
55
|
+
} catch {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Get Docker containers with AGENT_ID label — only Rev4a-managed agents */
|
|
61
|
+
function getAgents(): { agentId: string; containerName: string; state: string }[] {
|
|
62
|
+
try {
|
|
63
|
+
const raw = execSync(
|
|
64
|
+
`docker ps --filter "label=AGENT_ID" --format '{{.Label "AGENT_ID"}}|{{.Names}}|{{.State}}'`,
|
|
65
|
+
{ timeout: 5000, maxBuffer: 64 * 1024, encoding: 'utf-8' },
|
|
66
|
+
).trim();
|
|
67
|
+
if (!raw) return [];
|
|
68
|
+
return raw.split('\n').map((line) => {
|
|
69
|
+
const [agentId, containerName, state] = line.split('|');
|
|
70
|
+
return { agentId, containerName, state };
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Query an agent container for model status */
|
|
78
|
+
function getAgentStatus(agent: { agentId: string; containerName: string; state: string }): GatewayAgentStatus | null {
|
|
79
|
+
const raw = dockerExec(agent.containerName, 'openclaw models status --json', 8000);
|
|
80
|
+
if (!raw) return null;
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(raw);
|
|
83
|
+
const authProviders: Record<string, unknown>[] = data.auth?.providers ?? [];
|
|
84
|
+
const providers: GatewayProvider[] = authProviders.map((p: Record<string, unknown>) => ({
|
|
85
|
+
provider: String(p.provider ?? ''),
|
|
86
|
+
kind: String((p.effective as Record<string, unknown>)?.kind ?? ''),
|
|
87
|
+
detail: String((p.effective as Record<string, unknown>)?.detail ?? ''),
|
|
88
|
+
profiles: Number((p.profiles as Record<string, unknown>)?.count ?? 0),
|
|
89
|
+
labels: (p.profiles as Record<string, unknown>)?.labels as string[] ?? [],
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
const aliasesRaw = data.aliases ?? {};
|
|
93
|
+
const aliases: Record<string, string> = {};
|
|
94
|
+
for (const [k, v] of Object.entries(aliasesRaw)) {
|
|
95
|
+
aliases[k] = String(v);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
agentId: agent.agentId,
|
|
100
|
+
containerName: agent.containerName,
|
|
101
|
+
state: agent.state,
|
|
102
|
+
defaultModel: String(data.defaultModel ?? ''),
|
|
103
|
+
fallbacks: (data.fallbacks ?? []).map(String),
|
|
104
|
+
allowed: (data.allowed ?? []).map(String),
|
|
105
|
+
aliases,
|
|
106
|
+
providers,
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Get available models from an agent */
|
|
114
|
+
function getAgentModels(container: string): GatewayModel[] {
|
|
115
|
+
const raw = dockerExec(container, 'openclaw models list --json', 8000);
|
|
116
|
+
if (!raw) return [];
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
const models: Record<string, unknown>[] = parsed.models ?? parsed ?? [];
|
|
120
|
+
return models
|
|
121
|
+
.filter((m: Record<string, unknown>) => m.available !== false && !m.missing)
|
|
122
|
+
.map((m: Record<string, unknown>) => ({
|
|
123
|
+
key: String(m.key ?? ''),
|
|
124
|
+
provider: String(m.key ?? '').split('/')[0] || 'unknown',
|
|
125
|
+
model: String(m.name ?? String(m.key ?? '').split('/').slice(1).join('/')),
|
|
126
|
+
available: m.available !== false,
|
|
127
|
+
}));
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
134
|
+
try {
|
|
135
|
+
const timestamp = Date.now();
|
|
136
|
+
|
|
137
|
+
// Only Rev4a-managed agents (AGENT_ID label)
|
|
138
|
+
const agentsRaw = getAgents();
|
|
139
|
+
const agents = agentsRaw
|
|
140
|
+
.map((a) => getAgentStatus(a))
|
|
141
|
+
.filter(Boolean) as GatewayAgentStatus[];
|
|
142
|
+
|
|
143
|
+
// Aggregate models from all agents (deduplicated by key)
|
|
144
|
+
const modelMap = new Map<string, GatewayModel>();
|
|
145
|
+
for (const agent of agents) {
|
|
146
|
+
for (const m of getAgentModels(agent.containerName)) {
|
|
147
|
+
if (!modelMap.has(m.key)) modelMap.set(m.key, m);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const allModels = Array.from(modelMap.values());
|
|
151
|
+
const availableModels = allModels.filter((m) => m.available);
|
|
152
|
+
|
|
153
|
+
// Aggregate aliases from all agents
|
|
154
|
+
const combinedAliases: Record<string, string[]> = {};
|
|
155
|
+
for (const agent of agents) {
|
|
156
|
+
for (const [alias, model] of Object.entries(agent.aliases)) {
|
|
157
|
+
if (!combinedAliases[alias]) combinedAliases[alias] = [];
|
|
158
|
+
if (!combinedAliases[alias].includes(model)) {
|
|
159
|
+
combinedAliases[alias].push(model);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// First agent is the "primary" for core model settings
|
|
165
|
+
const primaryAgent = agents[0] ?? null;
|
|
166
|
+
|
|
167
|
+
// API keys from the first agent's providers
|
|
168
|
+
const apiKeys: ApiKeyStatus[] = [];
|
|
169
|
+
if (primaryAgent) {
|
|
170
|
+
for (const p of primaryAgent.providers) {
|
|
171
|
+
// We can't query apiKey presence from openclaw models status (it's redacted).
|
|
172
|
+
// Mark as configured if the provider appears in the auth list.
|
|
173
|
+
apiKeys.push({
|
|
174
|
+
provider: p.provider,
|
|
175
|
+
configured: p.profiles > 0,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Models by provider
|
|
181
|
+
const byProvider = allModels.reduce<Record<string, number>>((acc, m) => {
|
|
182
|
+
acc[m.provider] = (acc[m.provider] ?? 0) + 1;
|
|
183
|
+
return acc;
|
|
184
|
+
}, {});
|
|
185
|
+
|
|
186
|
+
return NextResponse.json({
|
|
187
|
+
timestamp,
|
|
188
|
+
gateway: 'Rev4a Model Gateway',
|
|
189
|
+
status: 'online',
|
|
190
|
+
models: {
|
|
191
|
+
total: allModels.length,
|
|
192
|
+
available: availableModels.length,
|
|
193
|
+
byProvider: Object.entries(byProvider).map(([provider, count]) => ({ provider, count })),
|
|
194
|
+
list: allModels,
|
|
195
|
+
},
|
|
196
|
+
agents: {
|
|
197
|
+
total: agents.length,
|
|
198
|
+
list: agents.map((a) => ({
|
|
199
|
+
agentId: a.agentId,
|
|
200
|
+
containerName: a.containerName,
|
|
201
|
+
state: a.state,
|
|
202
|
+
defaultModel: a.defaultModel,
|
|
203
|
+
fallbacks: a.fallbacks,
|
|
204
|
+
aliases: a.aliases,
|
|
205
|
+
providers: a.providers,
|
|
206
|
+
})),
|
|
207
|
+
},
|
|
208
|
+
aliases: combinedAliases,
|
|
209
|
+
coreModel: primaryAgent
|
|
210
|
+
? {
|
|
211
|
+
defaultModel: primaryAgent.defaultModel,
|
|
212
|
+
fallbacks: primaryAgent.fallbacks,
|
|
213
|
+
}
|
|
214
|
+
: null,
|
|
215
|
+
apiKeys: {
|
|
216
|
+
configured: apiKeys.filter((k) => k.configured).map((k) => k.provider),
|
|
217
|
+
all: apiKeys,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
} catch (e: unknown) {
|
|
221
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Gateway Sync
|
|
3
|
+
*
|
|
4
|
+
* Syncs the models.providers.rev4a config block to all agent containers
|
|
5
|
+
* (or a single container) without touching model references (primary/fallback).
|
|
6
|
+
*
|
|
7
|
+
* Import and call:
|
|
8
|
+
* syncAllAgents() — sync all containers with AGENT_ID label
|
|
9
|
+
* syncAgent('container') — sync a single container
|
|
10
|
+
*/
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import { readProviderKeys } from './provider/keys';
|
|
14
|
+
import { loadModelsConfig, type ModelConfigEntry } from '@/lib/model-catalogue';
|
|
15
|
+
import { detectProviderGatewayUrl } from '@/lib/docker-utils';
|
|
16
|
+
|
|
17
|
+
const PROVIDER_GATEWAY_BASE_URL = detectProviderGatewayUrl();
|
|
18
|
+
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
/* Helpers */
|
|
21
|
+
/* ------------------------------------------------------------------ */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert internal modality string (e.g. "text+image+file->text") to
|
|
25
|
+
* OpenClaw `input` array. Filters out values not in the allowed set
|
|
26
|
+
* (text | image | video | audio).
|
|
27
|
+
*/
|
|
28
|
+
const ALLOWED_INPUT = new Set(['text', 'image', 'video', 'audio']);
|
|
29
|
+
|
|
30
|
+
function modalityToInput(modality: string): string[] {
|
|
31
|
+
const arrowIdx = modality.indexOf('->');
|
|
32
|
+
const left = arrowIdx >= 0 ? modality.slice(0, arrowIdx) : modality;
|
|
33
|
+
return left.split('+').map((s) => s.trim()).filter((s) => ALLOWED_INPUT.has(s));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getActiveRev4aModels(models: ModelConfigEntry[]): { id: string; name: string; input?: string[] }[] {
|
|
37
|
+
const providerKeys = readProviderKeys();
|
|
38
|
+
const configuredProviders = Object.keys(providerKeys);
|
|
39
|
+
return models
|
|
40
|
+
.filter((m) => m.enabled && configuredProviders.includes(m.provider))
|
|
41
|
+
.map((m) => {
|
|
42
|
+
const entry: { id: string; name: string; input?: string[] } = { id: m.id, name: m.name };
|
|
43
|
+
if (m.modality) entry.input = modalityToInput(m.modality);
|
|
44
|
+
return entry;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getAgentContainers(): string[] {
|
|
49
|
+
try {
|
|
50
|
+
const raw = execSync(
|
|
51
|
+
`docker ps --filter "label=AGENT_ID" --format '{{.Names}}'`,
|
|
52
|
+
{ timeout: 5000, maxBuffer: 64 * 1024, encoding: 'utf-8' },
|
|
53
|
+
).trim();
|
|
54
|
+
if (!raw) return [];
|
|
55
|
+
return raw.split('\n');
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readContainerJson(container: string, remotePath: string): Record<string, unknown> {
|
|
62
|
+
try {
|
|
63
|
+
const raw = execSync(
|
|
64
|
+
`docker exec ${container} cat ${remotePath}`,
|
|
65
|
+
{ timeout: 8000, maxBuffer: 512 * 1024, encoding: 'utf-8' },
|
|
66
|
+
);
|
|
67
|
+
return JSON.parse(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeContainerJson(container: string, remotePath: string, data: Record<string, unknown>): void {
|
|
74
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
|
75
|
+
// Use shell-quoted heredoc to avoid word-splitting on paths with spaces
|
|
76
|
+
execSync(
|
|
77
|
+
`docker exec -i ${container} sh -c "cat > '${remotePath}'"`,
|
|
78
|
+
{ timeout: 10000, maxBuffer: 1024 * 1024, input: jsonStr },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function restartContainerGateway(container: string): string | null {
|
|
83
|
+
try {
|
|
84
|
+
execSync(`docker exec ${container} openclaw gateway restart`, { timeout: 15000, maxBuffer: 64 * 1024 });
|
|
85
|
+
return null;
|
|
86
|
+
} catch (e: unknown) {
|
|
87
|
+
const err = e as { stdout?: string; stderr?: string; message?: string };
|
|
88
|
+
return err.stderr?.toString().trim() || err.message || 'unknown error';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ------------------------------------------------------------------ */
|
|
93
|
+
/* Sync functions */
|
|
94
|
+
/* ------------------------------------------------------------------ */
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build the models.providers.rev4a config object from local state.
|
|
98
|
+
* Shared between the create wizard and syncAgent — single source of truth
|
|
99
|
+
* for the provider block injected into every agent container.
|
|
100
|
+
*/
|
|
101
|
+
export function buildRev4aProviderConfig(): { baseUrl: string; apiKey: string; api: string; models: { id: string; name: string; input?: string[] }[] } {
|
|
102
|
+
const providerKeys = readProviderKeys();
|
|
103
|
+
const rev4aApiKey = providerKeys['rev4a'] || '';
|
|
104
|
+
const allModels = loadModelsConfig();
|
|
105
|
+
const activeModels = getActiveRev4aModels(allModels);
|
|
106
|
+
return {
|
|
107
|
+
baseUrl: PROVIDER_GATEWAY_BASE_URL,
|
|
108
|
+
apiKey: rev4aApiKey,
|
|
109
|
+
api: 'openai-completions',
|
|
110
|
+
models: activeModels,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sync models.providers.rev4a to all agent containers.
|
|
116
|
+
* Does NOT touch agents.defaults.model or agents.list[].model.
|
|
117
|
+
*/
|
|
118
|
+
export function syncAllAgents(): string[] {
|
|
119
|
+
const rev4aProviderConfig = buildRev4aProviderConfig();
|
|
120
|
+
if (!rev4aProviderConfig.apiKey) {
|
|
121
|
+
return ['ERROR: REV4A_API_KEY not found in provider-keys.json (add "rev4a" key)'];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const containers = getAgentContainers();
|
|
125
|
+
const results: string[] = [`Active models: ${rev4aProviderConfig.models.length} across ${containers.length} agent(s)`];
|
|
126
|
+
|
|
127
|
+
for (const container of containers) {
|
|
128
|
+
try {
|
|
129
|
+
const result = syncContainer(container, rev4aProviderConfig);
|
|
130
|
+
results.push(result);
|
|
131
|
+
} catch (e: unknown) {
|
|
132
|
+
const err = e as { stderr?: string; message?: string };
|
|
133
|
+
results.push(`${container}: ERROR ${err.stderr || err.message || 'unknown'}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (containers.length === 0) {
|
|
138
|
+
results.push('No agent containers found');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sync models.providers.rev4a to a single agent container.
|
|
146
|
+
* Does NOT touch agents.defaults.model or agents.list[].model.
|
|
147
|
+
*/
|
|
148
|
+
export function syncAgent(containerName: string): string {
|
|
149
|
+
const rev4aProviderConfig = buildRev4aProviderConfig();
|
|
150
|
+
if (!rev4aProviderConfig.apiKey) {
|
|
151
|
+
throw new Error('REV4A_API_KEY not found in provider-keys.json (add "rev4a" key)');
|
|
152
|
+
}
|
|
153
|
+
return syncContainer(containerName, rev4aProviderConfig);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Internal: write models.providers.rev4a to one container and restart its gateway.
|
|
158
|
+
*/
|
|
159
|
+
function syncContainer(
|
|
160
|
+
container: string,
|
|
161
|
+
rev4aProviderConfig: { baseUrl: string; apiKey: string; api: string; models: { id: string; name: string }[] },
|
|
162
|
+
): string {
|
|
163
|
+
const remotePath = '/root/.openclaw/openclaw.json';
|
|
164
|
+
const config = readContainerJson(container, remotePath) as Record<string, unknown>;
|
|
165
|
+
|
|
166
|
+
// Clean up stale files from old versions
|
|
167
|
+
try {
|
|
168
|
+
execSync(
|
|
169
|
+
`docker exec ${container} sh -c 'rm -f /root/.openclaw/agents/main/agent/models.json /root/.openclaw/agents/main/agent/auth-profiles.json'`,
|
|
170
|
+
{ timeout: 5000 },
|
|
171
|
+
);
|
|
172
|
+
} catch {
|
|
173
|
+
// non-fatal
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Write/overwrite models.providers.rev4a — do NOT touch agents.*
|
|
177
|
+
const models = (config.models as Record<string, unknown>) || {};
|
|
178
|
+
if (!models.providers) models.providers = {};
|
|
179
|
+
(models.providers as Record<string, unknown>)['rev4a'] = rev4aProviderConfig;
|
|
180
|
+
config.models = models;
|
|
181
|
+
|
|
182
|
+
writeContainerJson(container, remotePath, config);
|
|
183
|
+
|
|
184
|
+
const gwError = restartContainerGateway(container);
|
|
185
|
+
if (gwError) {
|
|
186
|
+
return `${container}: file updated but gateway restart failed: ${gwError}`;
|
|
187
|
+
}
|
|
188
|
+
return `${container}: synced + restarted OK`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
function ensureLineageSchema(db: ReturnType<typeof openDb>): void {
|
|
6
|
+
db.prepare(
|
|
7
|
+
`CREATE TABLE IF NOT EXISTS lineage (
|
|
8
|
+
child_id TEXT PRIMARY KEY,
|
|
9
|
+
parent_id TEXT NOT NULL,
|
|
10
|
+
declared_at INTEGER NOT NULL,
|
|
11
|
+
agent_name TEXT,
|
|
12
|
+
label TEXT
|
|
13
|
+
)`,
|
|
14
|
+
).run();
|
|
15
|
+
|
|
16
|
+
const columns = new Set<string>(
|
|
17
|
+
(db.prepare('PRAGMA table_info(lineage)').all() as Array<{ name: string }>).map(
|
|
18
|
+
(column) => column.name,
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (!columns.has('agent_name')) {
|
|
23
|
+
db.exec('ALTER TABLE lineage ADD COLUMN agent_name TEXT');
|
|
24
|
+
}
|
|
25
|
+
if (!columns.has('label')) {
|
|
26
|
+
db.exec('ALTER TABLE lineage ADD COLUMN label TEXT');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
31
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
32
|
+
try {
|
|
33
|
+
const body = (await request.json()) as { childId?: string; parentId?: string; label?: string };
|
|
34
|
+
const childId = body?.childId?.trim();
|
|
35
|
+
const parentId = body?.parentId?.trim();
|
|
36
|
+
const lineageLabel = body?.label?.trim() || null;
|
|
37
|
+
|
|
38
|
+
if (!childId || !parentId) {
|
|
39
|
+
return NextResponse.json({ error: 'childId and parentId required' }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const db = openDb(false);
|
|
43
|
+
ensureLineageSchema(db);
|
|
44
|
+
db.prepare(
|
|
45
|
+
`INSERT OR REPLACE INTO lineage (child_id, parent_id, declared_at, agent_name, label)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
47
|
+
).run(childId, parentId, Date.now(), lineageLabel, lineageLabel);
|
|
48
|
+
db.prepare('UPDATE sessions SET parent_id = ? WHERE session_id = ?').run(parentId, childId);
|
|
49
|
+
db.close();
|
|
50
|
+
return NextResponse.json({ ok: true, childId, parentId, label: lineageLabel });
|
|
51
|
+
} catch (e: unknown) {
|
|
52
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { getMemoryContextSnapshot } from '@/lib/memory-context';
|
|
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
|
+
|
|
10
|
+
try {
|
|
11
|
+
return NextResponse.json(getMemoryContextSnapshot());
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
|
14
|
+
}
|
|
15
|
+
}
|