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.
Files changed (86) hide show
  1. package/.env.example +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +402 -2
  4. package/bin/ai-center.js +183 -0
  5. package/client/dist/assets/index-BRl_ORtB.css +1 -0
  6. package/client/dist/assets/index-CLhAmxoi.js +4444 -0
  7. package/client/dist/icon-192.png +0 -0
  8. package/client/dist/icon-512.png +0 -0
  9. package/client/dist/icon.svg +43 -0
  10. package/client/dist/index.html +31 -0
  11. package/client/dist/manifest.webmanifest +30 -0
  12. package/client/dist/sw.js +75 -0
  13. package/package.json +66 -4
  14. package/server/.env.example +9 -0
  15. package/server/dist/server/src/adapters/base.js +4 -0
  16. package/server/dist/server/src/adapters/customApi.js +353 -0
  17. package/server/dist/server/src/adapters/hermes.js +82 -0
  18. package/server/dist/server/src/adapters/preset.js +221 -0
  19. package/server/dist/server/src/adapters/router.js +32 -0
  20. package/server/dist/server/src/config/index.js +9 -0
  21. package/server/dist/server/src/db/index.js +180 -0
  22. package/server/dist/server/src/db/schemaSql.js +522 -0
  23. package/server/dist/server/src/index.js +241 -0
  24. package/server/dist/server/src/middleware/rateLimiter.js +76 -0
  25. package/server/dist/server/src/middleware/securityHeaders.js +21 -0
  26. package/server/dist/server/src/routes/actions.js +79 -0
  27. package/server/dist/server/src/routes/activities.js +35 -0
  28. package/server/dist/server/src/routes/agents.js +354 -0
  29. package/server/dist/server/src/routes/alerts.js +174 -0
  30. package/server/dist/server/src/routes/auth.js +138 -0
  31. package/server/dist/server/src/routes/dashboard.js +41 -0
  32. package/server/dist/server/src/routes/dataAnalyst.js +90 -0
  33. package/server/dist/server/src/routes/documents.js +242 -0
  34. package/server/dist/server/src/routes/domain.js +58 -0
  35. package/server/dist/server/src/routes/evals.js +56 -0
  36. package/server/dist/server/src/routes/mcp.js +160 -0
  37. package/server/dist/server/src/routes/memoryGraph.js +91 -0
  38. package/server/dist/server/src/routes/monitoring.js +41 -0
  39. package/server/dist/server/src/routes/notifications.js +88 -0
  40. package/server/dist/server/src/routes/orchestrator.js +54 -0
  41. package/server/dist/server/src/routes/pipeline.js +261 -0
  42. package/server/dist/server/src/routes/playground.js +91 -0
  43. package/server/dist/server/src/routes/projects.js +183 -0
  44. package/server/dist/server/src/routes/providers.js +403 -0
  45. package/server/dist/server/src/routes/rag.js +118 -0
  46. package/server/dist/server/src/routes/reports.js +123 -0
  47. package/server/dist/server/src/routes/schedules.js +165 -0
  48. package/server/dist/server/src/routes/skills.js +102 -0
  49. package/server/dist/server/src/routes/sre.js +84 -0
  50. package/server/dist/server/src/routes/swarm.js +165 -0
  51. package/server/dist/server/src/routes/system.js +199 -0
  52. package/server/dist/server/src/routes/tasks.js +314 -0
  53. package/server/dist/server/src/routes/warRooms.js +113 -0
  54. package/server/dist/server/src/routes/webResearch.js +60 -0
  55. package/server/dist/server/src/routes/webhooks.js +102 -0
  56. package/server/dist/server/src/routes/workflows.js +168 -0
  57. package/server/dist/server/src/services/actionManager.js +246 -0
  58. package/server/dist/server/src/services/alertingService.js +289 -0
  59. package/server/dist/server/src/services/dataAnalystService.js +463 -0
  60. package/server/dist/server/src/services/documentParserService.js +205 -0
  61. package/server/dist/server/src/services/domainManagerService.js +286 -0
  62. package/server/dist/server/src/services/embedding.js +178 -0
  63. package/server/dist/server/src/services/evalService.js +359 -0
  64. package/server/dist/server/src/services/logger.js +61 -0
  65. package/server/dist/server/src/services/mcpClientService.js +518 -0
  66. package/server/dist/server/src/services/memoryGraphService.js +545 -0
  67. package/server/dist/server/src/services/monitoring.js +90 -0
  68. package/server/dist/server/src/services/orchestrator.js +463 -0
  69. package/server/dist/server/src/services/ragService.js +308 -0
  70. package/server/dist/server/src/services/realtime.js +74 -0
  71. package/server/dist/server/src/services/sandbox.js +673 -0
  72. package/server/dist/server/src/services/schedulerService.js +548 -0
  73. package/server/dist/server/src/services/skillStudioService.js +366 -0
  74. package/server/dist/server/src/services/sreAgentService.js +405 -0
  75. package/server/dist/server/src/services/swarmArtifactService.js +258 -0
  76. package/server/dist/server/src/services/swarmService.js +623 -0
  77. package/server/dist/server/src/services/warRoomService.js +461 -0
  78. package/server/dist/server/src/services/webResearchService.js +295 -0
  79. package/server/dist/server/src/services/workflowEngine.js +611 -0
  80. package/server/dist/server/src/utils/auth.js +33 -0
  81. package/server/dist/server/src/utils/crypto.js +55 -0
  82. package/server/dist/server/src/utils/ssrf.js +112 -0
  83. package/server/dist/shared/constants.js +113 -0
  84. package/server/dist/shared/types.js +5 -0
  85. package/shared/constants.ts +129 -0
  86. 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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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,4 @@
1
+ /**
2
+ * Base Connection Adapter Interface — AI Agent Command Center
3
+ */
4
+ export {};
@@ -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
+ }