ai-agent-command-center 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +20 -0
- package/LICENSE +21 -0
- package/README.md +402 -2
- package/bin/ai-center.js +183 -0
- package/client/dist/assets/index-BRl_ORtB.css +1 -0
- package/client/dist/assets/index-CLhAmxoi.js +4444 -0
- package/client/dist/icon-192.png +0 -0
- package/client/dist/icon-512.png +0 -0
- package/client/dist/icon.svg +43 -0
- package/client/dist/index.html +31 -0
- package/client/dist/manifest.webmanifest +30 -0
- package/client/dist/sw.js +75 -0
- package/package.json +66 -4
- package/server/.env.example +9 -0
- package/server/dist/server/src/adapters/base.js +4 -0
- package/server/dist/server/src/adapters/customApi.js +353 -0
- package/server/dist/server/src/adapters/hermes.js +82 -0
- package/server/dist/server/src/adapters/preset.js +221 -0
- package/server/dist/server/src/adapters/router.js +32 -0
- package/server/dist/server/src/config/index.js +9 -0
- package/server/dist/server/src/db/index.js +180 -0
- package/server/dist/server/src/db/schemaSql.js +522 -0
- package/server/dist/server/src/index.js +241 -0
- package/server/dist/server/src/middleware/rateLimiter.js +76 -0
- package/server/dist/server/src/middleware/securityHeaders.js +21 -0
- package/server/dist/server/src/routes/actions.js +79 -0
- package/server/dist/server/src/routes/activities.js +35 -0
- package/server/dist/server/src/routes/agents.js +354 -0
- package/server/dist/server/src/routes/alerts.js +174 -0
- package/server/dist/server/src/routes/auth.js +138 -0
- package/server/dist/server/src/routes/dashboard.js +41 -0
- package/server/dist/server/src/routes/dataAnalyst.js +90 -0
- package/server/dist/server/src/routes/documents.js +242 -0
- package/server/dist/server/src/routes/domain.js +58 -0
- package/server/dist/server/src/routes/evals.js +56 -0
- package/server/dist/server/src/routes/mcp.js +160 -0
- package/server/dist/server/src/routes/memoryGraph.js +91 -0
- package/server/dist/server/src/routes/monitoring.js +41 -0
- package/server/dist/server/src/routes/notifications.js +88 -0
- package/server/dist/server/src/routes/orchestrator.js +54 -0
- package/server/dist/server/src/routes/pipeline.js +261 -0
- package/server/dist/server/src/routes/playground.js +91 -0
- package/server/dist/server/src/routes/projects.js +183 -0
- package/server/dist/server/src/routes/providers.js +403 -0
- package/server/dist/server/src/routes/rag.js +118 -0
- package/server/dist/server/src/routes/reports.js +123 -0
- package/server/dist/server/src/routes/schedules.js +165 -0
- package/server/dist/server/src/routes/skills.js +102 -0
- package/server/dist/server/src/routes/sre.js +84 -0
- package/server/dist/server/src/routes/swarm.js +165 -0
- package/server/dist/server/src/routes/system.js +199 -0
- package/server/dist/server/src/routes/tasks.js +314 -0
- package/server/dist/server/src/routes/warRooms.js +113 -0
- package/server/dist/server/src/routes/webResearch.js +60 -0
- package/server/dist/server/src/routes/webhooks.js +102 -0
- package/server/dist/server/src/routes/workflows.js +168 -0
- package/server/dist/server/src/services/actionManager.js +246 -0
- package/server/dist/server/src/services/alertingService.js +289 -0
- package/server/dist/server/src/services/dataAnalystService.js +463 -0
- package/server/dist/server/src/services/documentParserService.js +205 -0
- package/server/dist/server/src/services/domainManagerService.js +286 -0
- package/server/dist/server/src/services/embedding.js +178 -0
- package/server/dist/server/src/services/evalService.js +359 -0
- package/server/dist/server/src/services/logger.js +61 -0
- package/server/dist/server/src/services/mcpClientService.js +518 -0
- package/server/dist/server/src/services/memoryGraphService.js +545 -0
- package/server/dist/server/src/services/monitoring.js +90 -0
- package/server/dist/server/src/services/orchestrator.js +463 -0
- package/server/dist/server/src/services/ragService.js +308 -0
- package/server/dist/server/src/services/realtime.js +74 -0
- package/server/dist/server/src/services/sandbox.js +673 -0
- package/server/dist/server/src/services/schedulerService.js +548 -0
- package/server/dist/server/src/services/skillStudioService.js +366 -0
- package/server/dist/server/src/services/sreAgentService.js +405 -0
- package/server/dist/server/src/services/swarmArtifactService.js +258 -0
- package/server/dist/server/src/services/swarmService.js +623 -0
- package/server/dist/server/src/services/warRoomService.js +461 -0
- package/server/dist/server/src/services/webResearchService.js +295 -0
- package/server/dist/server/src/services/workflowEngine.js +611 -0
- package/server/dist/server/src/utils/auth.js +33 -0
- package/server/dist/server/src/utils/crypto.js +55 -0
- package/server/dist/server/src/utils/ssrf.js +112 -0
- package/server/dist/shared/constants.js +113 -0
- package/server/dist/shared/types.js +5 -0
- package/shared/constants.ts +129 -0
- package/shared/types.ts +1360 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#0B0F17"/>
|
|
5
|
+
<stop offset="100%" stop-color="#080C14"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="cyanGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
8
|
+
<stop offset="0%" stop-color="#06B6D4"/>
|
|
9
|
+
<stop offset="100%" stop-color="#3B82F6"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
|
|
12
|
+
<stop offset="0%" stop-color="#06B6D4" stop-opacity="0.4"/>
|
|
13
|
+
<stop offset="100%" stop-color="#0B0F17" stop-opacity="0"/>
|
|
14
|
+
</radialGradient>
|
|
15
|
+
</defs>
|
|
16
|
+
|
|
17
|
+
<!-- Background -->
|
|
18
|
+
<rect width="512" height="512" rx="112" fill="url(#bg)"/>
|
|
19
|
+
<circle cx="256" cy="256" r="220" fill="url(#glow)"/>
|
|
20
|
+
|
|
21
|
+
<!-- Outer Orbit Rings -->
|
|
22
|
+
<circle cx="256" cy="256" r="180" fill="none" stroke="#1E293B" stroke-width="4"/>
|
|
23
|
+
<circle cx="256" cy="256" r="180" fill="none" stroke="#06B6D4" stroke-width="4" stroke-dasharray="100 40 20 40"/>
|
|
24
|
+
|
|
25
|
+
<circle cx="256" cy="256" r="130" fill="none" stroke="#334155" stroke-width="2"/>
|
|
26
|
+
<circle cx="256" cy="256" r="130" fill="none" stroke="#3B82F6" stroke-width="3" stroke-dasharray="40 80"/>
|
|
27
|
+
|
|
28
|
+
<!-- Core Hexagon Command Nexus -->
|
|
29
|
+
<polygon points="256,150 350,204 350,308 256,362 162,308 162,204"
|
|
30
|
+
fill="#0F172A" stroke="url(#cyanGrad)" stroke-width="6"/>
|
|
31
|
+
|
|
32
|
+
<!-- Node Dots & Telemetry Links -->
|
|
33
|
+
<circle cx="256" cy="150" r="10" fill="#06B6D4"/>
|
|
34
|
+
<circle cx="350" cy="204" r="10" fill="#06B6D4"/>
|
|
35
|
+
<circle cx="350" cy="308" r="10" fill="#3B82F6"/>
|
|
36
|
+
<circle cx="256" cy="362" r="10" fill="#3B82F6"/>
|
|
37
|
+
<circle cx="162" cy="308" r="10" fill="#06B6D4"/>
|
|
38
|
+
<circle cx="162" cy="204" r="10" fill="#06B6D4"/>
|
|
39
|
+
|
|
40
|
+
<!-- Central Pulse Eye -->
|
|
41
|
+
<circle cx="256" cy="256" r="38" fill="url(#cyanGrad)"/>
|
|
42
|
+
<circle cx="256" cy="256" r="18" fill="#FFFFFF"/>
|
|
43
|
+
</svg>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="id" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
|
6
|
+
<link rel="apple-touch-icon" href="/icon-192.png" />
|
|
7
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
|
|
9
|
+
<meta name="theme-color" content="#0B0F17" />
|
|
10
|
+
<meta name="description" content="Virtual Command Center & Orchestration Platform for Autonomous AI Agents" />
|
|
11
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
12
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
|
+
<meta name="apple-mobile-web-app-title" content="AI Center" />
|
|
14
|
+
<title>AI Agent Command Center — Antigravity</title>
|
|
15
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
16
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
17
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
18
|
+
<script type="module" crossorigin src="/assets/index-CLhAmxoi.js"></script>
|
|
19
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BRl_ORtB.css">
|
|
20
|
+
</head>
|
|
21
|
+
<body class="bg-[#0B0F17] text-slate-100 font-['Plus_Jakarta_Sans',sans-serif] antialiased min-h-screen selection:bg-cyan-500/30 selection:text-white">
|
|
22
|
+
<div id="root"></div>
|
|
23
|
+
<script>
|
|
24
|
+
if ('serviceWorker' in navigator && window.location.protocol.startsWith('http')) {
|
|
25
|
+
window.addEventListener('load', () => {
|
|
26
|
+
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "AI Agent Command Center",
|
|
3
|
+
"short_name": "AI Center",
|
|
4
|
+
"description": "Virtual Command Center & Orchestration Platform for Autonomous AI Agents",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#0B0F17",
|
|
8
|
+
"theme_color": "#0B0F17",
|
|
9
|
+
"orientation": "any",
|
|
10
|
+
"icons": [
|
|
11
|
+
{
|
|
12
|
+
"src": "/icon-192.png",
|
|
13
|
+
"sizes": "192x192",
|
|
14
|
+
"type": "image/png",
|
|
15
|
+
"purpose": "any maskable"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"src": "/icon-512.png",
|
|
19
|
+
"sizes": "512x512",
|
|
20
|
+
"type": "image/png",
|
|
21
|
+
"purpose": "any maskable"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"src": "/icon.svg",
|
|
25
|
+
"sizes": "any",
|
|
26
|
+
"type": "image/svg+xml",
|
|
27
|
+
"purpose": "any"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Worker — AI Agent Command Center PWA
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CACHE_NAME = 'ai-center-cache-v1';
|
|
6
|
+
const PRECACHE_ASSETS = [
|
|
7
|
+
'/',
|
|
8
|
+
'/index.html',
|
|
9
|
+
'/manifest.webmanifest',
|
|
10
|
+
'/icon.svg',
|
|
11
|
+
'/icon-192.png',
|
|
12
|
+
'/icon-512.png'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
self.addEventListener('install', (event) => {
|
|
16
|
+
event.waitUntil(
|
|
17
|
+
caches.open(CACHE_NAME).then((cache) => {
|
|
18
|
+
return cache.addAll(PRECACHE_ASSETS);
|
|
19
|
+
}).then(() => self.skipWaiting())
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
self.addEventListener('activate', (event) => {
|
|
24
|
+
event.waitUntil(
|
|
25
|
+
caches.keys().then((keys) => {
|
|
26
|
+
return Promise.all(
|
|
27
|
+
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
|
|
28
|
+
);
|
|
29
|
+
}).then(() => self.clients.claim())
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
self.addEventListener('fetch', (event) => {
|
|
34
|
+
const url = new URL(event.request.url);
|
|
35
|
+
|
|
36
|
+
// Bypass API and SSE streams from caching
|
|
37
|
+
if (url.pathname.startsWith('/api') || url.pathname.startsWith('/healthz')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Network-first for html navigation, cache fallback
|
|
42
|
+
if (event.request.mode === 'navigate') {
|
|
43
|
+
event.respondWith(
|
|
44
|
+
fetch(event.request).catch(() => {
|
|
45
|
+
return caches.match('/index.html') || caches.match('/');
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Stale-while-revalidate for static assets
|
|
52
|
+
event.respondWith(
|
|
53
|
+
caches.match(event.request).then((cachedResponse) => {
|
|
54
|
+
if (cachedResponse) {
|
|
55
|
+
fetch(event.request).then((networkResponse) => {
|
|
56
|
+
if (networkResponse && networkResponse.status === 200) {
|
|
57
|
+
caches.open(CACHE_NAME).then((cache) => {
|
|
58
|
+
cache.put(event.request, networkResponse);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}).catch(() => {});
|
|
62
|
+
return cachedResponse;
|
|
63
|
+
}
|
|
64
|
+
return fetch(event.request).then((networkResponse) => {
|
|
65
|
+
if (networkResponse && networkResponse.status === 200 && event.request.method === 'GET') {
|
|
66
|
+
const responseToCache = networkResponse.clone();
|
|
67
|
+
caches.open(CACHE_NAME).then((cache) => {
|
|
68
|
+
cache.put(event.request, responseToCache);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return networkResponse;
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-agent-command-center",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Next-Generation Spatial 3D/2D Orchestration Bridge & Autonomous Multi-Agent Swarm OS",
|
|
5
|
+
"main": "server/dist/server/src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ai-center": "./bin/ai-center.js",
|
|
9
|
+
"ai-agent-command-center": "./bin/ai-center.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"server/dist",
|
|
14
|
+
"client/dist",
|
|
15
|
+
"shared",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
".env.example"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"workspaces": [
|
|
24
|
+
"server",
|
|
25
|
+
"client"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev:server": "npm run dev -w server",
|
|
29
|
+
"dev:client": "npm run dev -w client",
|
|
30
|
+
"build": "npm run build -w server && npm run build -w client",
|
|
31
|
+
"test": "npm run test -w server",
|
|
32
|
+
"start": "node bin/ai-center.js",
|
|
33
|
+
"prepack": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"cors": "^2.8.5",
|
|
37
|
+
"dotenv": "^16.4.5",
|
|
38
|
+
"express": "^4.19.2",
|
|
39
|
+
"pg": "^8.13.0",
|
|
40
|
+
"uuid": "^9.0.1"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/NexGen-X/Ai-Center-Workspace.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/NexGen-X/Ai-Center-Workspace#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/NexGen-X/Ai-Center-Workspace/issues"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"ai",
|
|
52
|
+
"autonomous-agents",
|
|
53
|
+
"swarm",
|
|
54
|
+
"multi-agent",
|
|
55
|
+
"command-center",
|
|
56
|
+
"threejs",
|
|
57
|
+
"webgl",
|
|
58
|
+
"pgvector",
|
|
59
|
+
"rag",
|
|
60
|
+
"sre",
|
|
61
|
+
"orchestrator"
|
|
62
|
+
],
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"author": "NexGen-X / Antigravity AI Center",
|
|
67
|
+
"license": "MIT"
|
|
68
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# AI Agent Command Center — Server Environment Configuration Template
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
PORT=3000
|
|
6
|
+
NODE_ENV=production
|
|
7
|
+
DATABASE_URL=postgres://routex:your_db_password_here@127.0.0.1:5432/ai_center?sslmode=disable
|
|
8
|
+
ENCRYPTION_KEY=replace-with-your-secure-32-character-secret-key-here!
|
|
9
|
+
DEFAULT_WORKSPACE_ID=ws-default-command-center
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom API Connection Adapter — AI Agent Command Center
|
|
3
|
+
* Supports OpenAI-compatible and Anthropic-compatible custom endpoints with SSRF protection.
|
|
4
|
+
*/
|
|
5
|
+
import { validateOutgoingUrl } from '../utils/ssrf.js';
|
|
6
|
+
export class CustomApiAdapter {
|
|
7
|
+
async testConnection(config) {
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
const urlValidation = validateOutgoingUrl(config.baseUrl);
|
|
10
|
+
if (!urlValidation.isValid) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
status: 'CONNECTION_FAILED',
|
|
14
|
+
message: urlValidation.error || 'Base URL tidak valid.'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
// 1. Try model discovery if supported (OpenAI standard GET /models or /v1/models)
|
|
19
|
+
const models = await this.discoverModels(config);
|
|
20
|
+
const latencyMs = Date.now() - startTime;
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
status: models.length > 0 ? 'MODEL_VERIFIED' : 'CONNECTION_OK',
|
|
24
|
+
message: models.length > 0
|
|
25
|
+
? `Koneksi berhasil. Ditemukan ${models.length} model dari provider.`
|
|
26
|
+
: 'Koneksi ke endpoint berhasil terverifikasi.',
|
|
27
|
+
discoveredModels: models,
|
|
28
|
+
latencyMs
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const latencyMs = Date.now() - startTime;
|
|
33
|
+
// Fallback: If GET /models fails (some endpoints only support completions), attempt a minimal completion probe
|
|
34
|
+
try {
|
|
35
|
+
const probeResult = await this.probeChatCompletion(config);
|
|
36
|
+
if (probeResult.success) {
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
status: 'CONNECTION_OK',
|
|
40
|
+
message: 'Endpoint chat completions merespons dengan baik (model discovery tidak tersedia).',
|
|
41
|
+
latencyMs: Date.now() - startTime
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore probe failure and return original error
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
status: 'CONNECTION_FAILED',
|
|
51
|
+
message: `Gagal terhubung ke endpoint: ${err.message}`,
|
|
52
|
+
latencyMs
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async discoverModels(config) {
|
|
57
|
+
const urlValidation = validateOutgoingUrl(config.baseUrl);
|
|
58
|
+
if (!urlValidation.isValid)
|
|
59
|
+
return [];
|
|
60
|
+
let targetUrl = config.baseUrl.replace(/\/+$/, '');
|
|
61
|
+
if (!targetUrl.endsWith('/models')) {
|
|
62
|
+
targetUrl = targetUrl.endsWith('/v1') ? `${targetUrl}/models` : `${targetUrl}/v1/models`;
|
|
63
|
+
}
|
|
64
|
+
const headers = {
|
|
65
|
+
'Content-Type': 'application/json'
|
|
66
|
+
};
|
|
67
|
+
if (config.apiKey) {
|
|
68
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
69
|
+
}
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeout = setTimeout(() => controller.abort(), config.timeoutMs || 8000);
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(targetUrl, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers,
|
|
76
|
+
signal: controller.signal
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorText = await response.text();
|
|
80
|
+
throw new Error(`HTTP ${response.status}: ${errorText.slice(0, 200)}`);
|
|
81
|
+
}
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
if (Array.isArray(data.data)) {
|
|
84
|
+
return data.data.map((m) => m.id || m.name).filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async probeChatCompletion(config) {
|
|
93
|
+
let targetUrl = config.baseUrl.replace(/\/+$/, '');
|
|
94
|
+
if (!targetUrl.endsWith('/chat/completions')) {
|
|
95
|
+
targetUrl = targetUrl.endsWith('/v1') ? `${targetUrl}/chat/completions` : `${targetUrl}/v1/chat/completions`;
|
|
96
|
+
}
|
|
97
|
+
const headers = {
|
|
98
|
+
'Content-Type': 'application/json'
|
|
99
|
+
};
|
|
100
|
+
if (config.apiKey) {
|
|
101
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
102
|
+
}
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeout = setTimeout(() => controller.abort(), 6000);
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(targetUrl, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers,
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
model: 'test',
|
|
111
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
112
|
+
max_tokens: 1
|
|
113
|
+
}),
|
|
114
|
+
signal: controller.signal
|
|
115
|
+
});
|
|
116
|
+
// Even a 400 with "model not found" proves network connection and auth were processed
|
|
117
|
+
if (response.status === 200 || response.status === 400 || response.status === 404) {
|
|
118
|
+
return { success: true };
|
|
119
|
+
}
|
|
120
|
+
return { success: false };
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(timeout);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async execute(config, modelId, systemPrompt, userPrompt, params, onChunk) {
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
const urlValidation = validateOutgoingUrl(config.baseUrl);
|
|
129
|
+
if (!urlValidation.isValid) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
status: 'FAILED',
|
|
133
|
+
errorCode: 'INVALID_URL',
|
|
134
|
+
errorMessage: urlValidation.error,
|
|
135
|
+
durationMs: 0
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const isAnthropic = config.protocol === 'anthropic';
|
|
139
|
+
let targetUrl = config.baseUrl.replace(/\/+$/, '');
|
|
140
|
+
const controller = new AbortController();
|
|
141
|
+
const timeout = setTimeout(() => controller.abort(), config.timeoutMs || 45000);
|
|
142
|
+
try {
|
|
143
|
+
if (isAnthropic) {
|
|
144
|
+
// Anthropic Messages API format
|
|
145
|
+
if (!targetUrl.endsWith('/messages')) {
|
|
146
|
+
targetUrl = `${targetUrl}/v1/messages`;
|
|
147
|
+
}
|
|
148
|
+
const headers = {
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
'anthropic-version': '2023-06-01'
|
|
151
|
+
};
|
|
152
|
+
if (config.apiKey)
|
|
153
|
+
headers['x-api-key'] = config.apiKey;
|
|
154
|
+
const response = await fetch(targetUrl, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers,
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
model: modelId,
|
|
159
|
+
system: systemPrompt,
|
|
160
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
161
|
+
max_tokens: params?.maxTokens || 2048,
|
|
162
|
+
temperature: params?.temperature ?? 0.7,
|
|
163
|
+
stream: Boolean(onChunk)
|
|
164
|
+
}),
|
|
165
|
+
signal: controller.signal
|
|
166
|
+
});
|
|
167
|
+
const durationMs = Date.now() - startTime;
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const errText = await response.text();
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
status: 'FAILED',
|
|
173
|
+
errorCode: `HTTP_${response.status}`,
|
|
174
|
+
errorMessage: errText.slice(0, 300),
|
|
175
|
+
durationMs
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (onChunk && response.body) {
|
|
179
|
+
const reader = response.body.getReader();
|
|
180
|
+
const decoder = new TextDecoder('utf-8');
|
|
181
|
+
let textOutput = '';
|
|
182
|
+
let buffer = '';
|
|
183
|
+
while (true) {
|
|
184
|
+
const { done, value } = await reader.read();
|
|
185
|
+
if (done)
|
|
186
|
+
break;
|
|
187
|
+
buffer += decoder.decode(value, { stream: true });
|
|
188
|
+
const lines = buffer.split('\n');
|
|
189
|
+
buffer = lines.pop() || '';
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
193
|
+
continue;
|
|
194
|
+
if (trimmed === 'data: [DONE]')
|
|
195
|
+
continue;
|
|
196
|
+
if (trimmed.startsWith('data: ')) {
|
|
197
|
+
try {
|
|
198
|
+
const json = JSON.parse(trimmed.slice(6));
|
|
199
|
+
if (json.type === 'content_block_delta' && json.delta?.text) {
|
|
200
|
+
textOutput += json.delta.text;
|
|
201
|
+
onChunk(json.delta.text);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// ignore partial JSON chunk
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const estimatedTokens = Math.ceil(textOutput.length / 4);
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
status: 'SUCCEEDED',
|
|
214
|
+
outputResult: textOutput,
|
|
215
|
+
durationMs,
|
|
216
|
+
tokensUsed: {
|
|
217
|
+
promptTokens: Math.ceil(userPrompt.length / 4),
|
|
218
|
+
completionTokens: estimatedTokens,
|
|
219
|
+
totalTokens: Math.ceil((userPrompt.length + textOutput.length) / 4)
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const data = await response.json();
|
|
224
|
+
const textOutput = data.content?.[0]?.text || '';
|
|
225
|
+
if (onChunk && textOutput) {
|
|
226
|
+
onChunk(textOutput);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
success: true,
|
|
230
|
+
status: 'SUCCEEDED',
|
|
231
|
+
outputResult: textOutput,
|
|
232
|
+
durationMs,
|
|
233
|
+
tokensUsed: {
|
|
234
|
+
promptTokens: data.usage?.input_tokens || 0,
|
|
235
|
+
completionTokens: data.usage?.output_tokens || 0,
|
|
236
|
+
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0)
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// OpenAI Chat Completions API format
|
|
242
|
+
if (!targetUrl.endsWith('/chat/completions')) {
|
|
243
|
+
targetUrl = targetUrl.endsWith('/v1') ? `${targetUrl}/chat/completions` : `${targetUrl}/v1/chat/completions`;
|
|
244
|
+
}
|
|
245
|
+
const headers = {
|
|
246
|
+
'Content-Type': 'application/json'
|
|
247
|
+
};
|
|
248
|
+
if (config.apiKey)
|
|
249
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
250
|
+
const response = await fetch(targetUrl, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers,
|
|
253
|
+
body: JSON.stringify({
|
|
254
|
+
model: modelId,
|
|
255
|
+
messages: [
|
|
256
|
+
{ role: 'system', content: systemPrompt },
|
|
257
|
+
{ role: 'user', content: userPrompt }
|
|
258
|
+
],
|
|
259
|
+
temperature: params?.temperature ?? 0.7,
|
|
260
|
+
max_tokens: params?.maxTokens || 2048,
|
|
261
|
+
stream: Boolean(onChunk)
|
|
262
|
+
}),
|
|
263
|
+
signal: controller.signal
|
|
264
|
+
});
|
|
265
|
+
const durationMs = Date.now() - startTime;
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
const errText = await response.text();
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
status: 'FAILED',
|
|
271
|
+
errorCode: `HTTP_${response.status}`,
|
|
272
|
+
errorMessage: errText.slice(0, 300),
|
|
273
|
+
durationMs
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (onChunk && response.body) {
|
|
277
|
+
const reader = response.body.getReader();
|
|
278
|
+
const decoder = new TextDecoder('utf-8');
|
|
279
|
+
let textOutput = '';
|
|
280
|
+
let buffer = '';
|
|
281
|
+
while (true) {
|
|
282
|
+
const { done, value } = await reader.read();
|
|
283
|
+
if (done)
|
|
284
|
+
break;
|
|
285
|
+
buffer += decoder.decode(value, { stream: true });
|
|
286
|
+
const lines = buffer.split('\n');
|
|
287
|
+
buffer = lines.pop() || '';
|
|
288
|
+
for (const line of lines) {
|
|
289
|
+
const trimmed = line.trim();
|
|
290
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
291
|
+
continue;
|
|
292
|
+
if (trimmed === 'data: [DONE]')
|
|
293
|
+
continue;
|
|
294
|
+
if (trimmed.startsWith('data: ')) {
|
|
295
|
+
try {
|
|
296
|
+
const json = JSON.parse(trimmed.slice(6));
|
|
297
|
+
const delta = json.choices?.[0]?.delta?.content;
|
|
298
|
+
if (delta) {
|
|
299
|
+
textOutput += delta;
|
|
300
|
+
onChunk(delta);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// ignore partial JSON chunk
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const estimatedTokens = Math.ceil(textOutput.length / 4);
|
|
310
|
+
return {
|
|
311
|
+
success: true,
|
|
312
|
+
status: 'SUCCEEDED',
|
|
313
|
+
outputResult: textOutput,
|
|
314
|
+
durationMs,
|
|
315
|
+
tokensUsed: {
|
|
316
|
+
promptTokens: Math.ceil(userPrompt.length / 4),
|
|
317
|
+
completionTokens: estimatedTokens,
|
|
318
|
+
totalTokens: Math.ceil((userPrompt.length + textOutput.length) / 4)
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const data = await response.json();
|
|
323
|
+
const textOutput = data.choices?.[0]?.message?.content || '';
|
|
324
|
+
if (onChunk && textOutput) {
|
|
325
|
+
onChunk(textOutput);
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
success: true,
|
|
329
|
+
status: 'SUCCEEDED',
|
|
330
|
+
outputResult: textOutput,
|
|
331
|
+
durationMs,
|
|
332
|
+
tokensUsed: {
|
|
333
|
+
promptTokens: data.usage?.prompt_tokens || 0,
|
|
334
|
+
completionTokens: data.usage?.completion_tokens || 0,
|
|
335
|
+
totalTokens: data.usage?.total_tokens || 0
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
return {
|
|
342
|
+
success: false,
|
|
343
|
+
status: 'FAILED',
|
|
344
|
+
errorCode: err.name === 'AbortError' ? 'TIMEOUT' : 'NETWORK_ERROR',
|
|
345
|
+
errorMessage: err.message,
|
|
346
|
+
durationMs: Date.now() - startTime
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
clearTimeout(timeout);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|