@relayplane/proxy 1.3.2 → 1.5.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/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/mesh.d.ts +55 -0
- package/dist/mesh.d.ts.map +1 -0
- package/dist/mesh.js +163 -0
- package/dist/mesh.js.map +1 -0
- package/dist/relay-config.d.ts +18 -0
- package/dist/relay-config.d.ts.map +1 -1
- package/dist/relay-config.js +15 -1
- package/dist/relay-config.js.map +1 -1
- package/dist/server.d.ts +244 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1261 -560
- package/dist/server.js.map +1 -1
- package/package.json +6 -2
package/dist/server.js
CHANGED
|
@@ -1,637 +1,1338 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* RelayPlane
|
|
3
|
+
* RelayPlane Agent Ops Proxy Server
|
|
4
|
+
*
|
|
5
|
+
* OpenAI-compatible proxy server with integrated observability via the Learning Ledger
|
|
6
|
+
* and auth enforcement via Auth Gate.
|
|
4
7
|
*
|
|
5
|
-
* Routes OpenAI-compatible requests to multiple providers.
|
|
6
8
|
* Features:
|
|
7
|
-
* - /
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
9
|
+
* - OpenAI-compatible `/v1/chat/completions` endpoint
|
|
10
|
+
* - Auth Gate integration for consumer vs API auth detection
|
|
11
|
+
* - Learning Ledger integration for run tracking
|
|
12
|
+
* - Timing capture (latency_ms, ttft_ms)
|
|
13
|
+
* - Structured error handling
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
11
16
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.ProxyServer = void 0;
|
|
52
|
+
exports.createProxyServer = createProxyServer;
|
|
53
|
+
exports.createSandboxedProxyServer = createSandboxedProxyServer;
|
|
54
|
+
const http = __importStar(require("node:http"));
|
|
55
|
+
const ledger_1 = require("@relayplane/ledger");
|
|
56
|
+
const auth_gate_1 = require("@relayplane/auth-gate");
|
|
57
|
+
const policy_engine_1 = require("@relayplane/policy-engine");
|
|
58
|
+
const routing_engine_1 = require("@relayplane/routing-engine");
|
|
59
|
+
const explainability_1 = require("@relayplane/explainability");
|
|
60
|
+
const learning_engine_1 = require("@relayplane/learning-engine");
|
|
61
|
+
const middleware_js_1 = require("./middleware.js");
|
|
62
|
+
const relay_config_js_1 = require("./relay-config.js");
|
|
63
|
+
const mesh_js_1 = require("./mesh.js");
|
|
33
64
|
/**
|
|
34
|
-
*
|
|
65
|
+
* Provider endpoint configuration
|
|
35
66
|
*/
|
|
36
|
-
const
|
|
37
|
-
'rp:fast': { model: 'llama-3.1-8b-instant', provider: 'groq' },
|
|
38
|
-
'rp:cheap': { model: 'llama-3.1-8b-instant', provider: 'groq' },
|
|
39
|
-
'rp:best': { model: 'claude-3-5-sonnet-20241022', provider: 'anthropic' },
|
|
40
|
-
'rp:balanced': { model: 'gpt-4o-mini', provider: 'openai' },
|
|
41
|
-
};
|
|
42
|
-
const PROVIDERS = {
|
|
43
|
-
openai: {
|
|
44
|
-
baseUrl: 'https://api.openai.com',
|
|
45
|
-
apiKeyEnv: 'OPENAI_API_KEY',
|
|
46
|
-
headerName: 'Authorization',
|
|
47
|
-
headerPrefix: 'Bearer ',
|
|
48
|
-
},
|
|
67
|
+
const PROVIDER_ENDPOINTS = {
|
|
49
68
|
anthropic: {
|
|
50
|
-
baseUrl: 'https://api.anthropic.com',
|
|
51
|
-
|
|
52
|
-
headerName: 'x-api-key',
|
|
53
|
-
headerPrefix: '',
|
|
54
|
-
extraHeaders: {
|
|
55
|
-
'anthropic-version': '2023-06-01',
|
|
56
|
-
},
|
|
69
|
+
baseUrl: 'https://api.anthropic.com/v1',
|
|
70
|
+
authHeader: 'x-api-key',
|
|
57
71
|
},
|
|
58
|
-
|
|
59
|
-
baseUrl: 'https://api.
|
|
60
|
-
|
|
61
|
-
headerName: 'Authorization',
|
|
62
|
-
headerPrefix: 'Bearer ',
|
|
63
|
-
},
|
|
64
|
-
together: {
|
|
65
|
-
baseUrl: 'https://api.together.xyz',
|
|
66
|
-
apiKeyEnv: 'TOGETHER_API_KEY',
|
|
67
|
-
headerName: 'Authorization',
|
|
68
|
-
headerPrefix: 'Bearer ',
|
|
72
|
+
openai: {
|
|
73
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
74
|
+
authHeader: 'Authorization',
|
|
69
75
|
},
|
|
70
76
|
openrouter: {
|
|
71
|
-
baseUrl: 'https://openrouter.ai/api',
|
|
72
|
-
|
|
73
|
-
headerName: 'Authorization',
|
|
74
|
-
headerPrefix: 'Bearer ',
|
|
77
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
78
|
+
authHeader: 'Authorization',
|
|
75
79
|
},
|
|
76
80
|
};
|
|
77
81
|
/**
|
|
78
|
-
* Model to provider mapping
|
|
82
|
+
* Model to provider mapping
|
|
79
83
|
*/
|
|
80
|
-
function
|
|
81
|
-
if (model.startsWith('
|
|
82
|
-
return 'openai';
|
|
83
|
-
if (model.startsWith('claude-'))
|
|
84
|
+
function getProviderForModel(model) {
|
|
85
|
+
if (model.startsWith('claude') || model.startsWith('anthropic')) {
|
|
84
86
|
return 'anthropic';
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return 'openai'; // Default fallback
|
|
87
|
+
}
|
|
88
|
+
if (model.startsWith('gpt') || model.startsWith('o1') || model.startsWith('o3')) {
|
|
89
|
+
return 'openai';
|
|
90
|
+
}
|
|
91
|
+
// Default to openrouter for other models
|
|
92
|
+
return 'openrouter';
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
94
|
-
*
|
|
95
|
+
* RelayPlane Agent Ops Proxy Server
|
|
95
96
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
class ProxyServer {
|
|
98
|
+
server = null;
|
|
99
|
+
ledger;
|
|
100
|
+
authGate;
|
|
101
|
+
policyEngine;
|
|
102
|
+
routingEngine;
|
|
103
|
+
capabilityRegistry;
|
|
104
|
+
providerManager;
|
|
105
|
+
explainer;
|
|
106
|
+
comparator;
|
|
107
|
+
simulator;
|
|
108
|
+
learningEngine = null;
|
|
109
|
+
meshHandle = null;
|
|
110
|
+
meshConfig;
|
|
111
|
+
config;
|
|
112
|
+
constructor(config = {}) {
|
|
113
|
+
this.config = {
|
|
114
|
+
port: config.port ?? 4801,
|
|
115
|
+
host: config.host ?? '127.0.0.1',
|
|
116
|
+
verbose: config.verbose ?? false,
|
|
117
|
+
defaultWorkspaceId: config.defaultWorkspaceId ?? 'default',
|
|
118
|
+
defaultAgentId: config.defaultAgentId ?? 'default',
|
|
119
|
+
defaultAuthEnforcementMode: config.defaultAuthEnforcementMode ?? 'recommended',
|
|
120
|
+
enforcePolicies: config.enforcePolicies ?? true,
|
|
121
|
+
enableRouting: config.enableRouting ?? true,
|
|
122
|
+
enableLearning: config.enableLearning ?? false,
|
|
123
|
+
enableMesh: config.enableMesh ?? true,
|
|
124
|
+
...config,
|
|
125
|
+
};
|
|
126
|
+
// Initialize mesh config
|
|
127
|
+
this.meshConfig = (0, relay_config_js_1.resolveMeshConfig)(config.meshConfig);
|
|
128
|
+
// Initialize ledger
|
|
129
|
+
this.ledger = config.ledger ?? (0, ledger_1.createLedger)();
|
|
130
|
+
// Initialize auth storage and gate
|
|
131
|
+
const authStorage = config.authStorage ?? new auth_gate_1.MemoryAuthProfileStorage();
|
|
132
|
+
this.authGate = (0, auth_gate_1.createAuthGate)({
|
|
133
|
+
storage: authStorage,
|
|
134
|
+
ledger: this.ledger,
|
|
135
|
+
defaultSettings: {
|
|
136
|
+
auth_enforcement_mode: this.config.defaultAuthEnforcementMode,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
// Initialize policy engine
|
|
140
|
+
const policyStorage = config.policyStorage ?? new policy_engine_1.MemoryPolicyStorage();
|
|
141
|
+
this.policyEngine = config.policyEngine ?? (0, policy_engine_1.createPolicyEngine)({
|
|
142
|
+
storage: policyStorage,
|
|
143
|
+
ledger: this.ledger,
|
|
144
|
+
});
|
|
145
|
+
// Initialize routing engine (Phase 3)
|
|
146
|
+
this.capabilityRegistry = config.capabilityRegistry ?? (0, routing_engine_1.createCapabilityRegistry)({
|
|
147
|
+
providerOverrides: this.buildProviderOverrides(),
|
|
148
|
+
});
|
|
149
|
+
this.providerManager = config.providerManager ?? (0, routing_engine_1.createProviderManagerWithBuiltIns)();
|
|
150
|
+
this.routingEngine = config.routingEngine ?? (0, routing_engine_1.createRoutingEngine)({
|
|
151
|
+
registry: this.capabilityRegistry,
|
|
152
|
+
ledger: this.ledger,
|
|
153
|
+
});
|
|
154
|
+
// Initialize explainability components (Phase 4)
|
|
155
|
+
this.explainer = (0, explainability_1.createExplanationEngine)({
|
|
156
|
+
ledger: this.ledger,
|
|
157
|
+
policyEngine: this.policyEngine,
|
|
158
|
+
routingEngine: this.routingEngine,
|
|
159
|
+
capabilityRegistry: this.capabilityRegistry,
|
|
160
|
+
});
|
|
161
|
+
this.comparator = (0, explainability_1.createRunComparator)({
|
|
162
|
+
ledger: this.ledger,
|
|
163
|
+
explanationEngine: this.explainer,
|
|
164
|
+
});
|
|
165
|
+
this.simulator = (0, explainability_1.createSimulator)({
|
|
166
|
+
policyEngine: this.policyEngine,
|
|
167
|
+
routingEngine: this.routingEngine,
|
|
168
|
+
capabilityRegistry: this.capabilityRegistry,
|
|
169
|
+
});
|
|
170
|
+
// Initialize learning engine (Phase 5)
|
|
171
|
+
if (this.config.enableLearning) {
|
|
172
|
+
this.learningEngine = config.learningEngine ?? (0, learning_engine_1.createLearningEngine)(this.ledger.getStorage(), undefined, config.learningConfig);
|
|
109
173
|
}
|
|
174
|
+
// Set API keys from config
|
|
175
|
+
this.configureProviderApiKeys();
|
|
110
176
|
}
|
|
111
|
-
|
|
112
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Build provider overrides from config
|
|
179
|
+
*/
|
|
180
|
+
buildProviderOverrides() {
|
|
181
|
+
const overrides = {};
|
|
182
|
+
const providers = this.config.providers ?? {};
|
|
183
|
+
if (providers.anthropic) {
|
|
184
|
+
overrides.anthropic = {
|
|
185
|
+
enabled: true,
|
|
186
|
+
base_url: providers.anthropic.baseUrl,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (providers.openai) {
|
|
190
|
+
overrides.openai = {
|
|
191
|
+
enabled: true,
|
|
192
|
+
base_url: providers.openai.baseUrl,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (providers.openrouter) {
|
|
196
|
+
overrides.openrouter = {
|
|
197
|
+
enabled: true,
|
|
198
|
+
base_url: providers.openrouter.baseUrl,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (providers.google) {
|
|
202
|
+
overrides.google = {
|
|
203
|
+
enabled: true,
|
|
204
|
+
base_url: providers.google.baseUrl,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (providers.together) {
|
|
208
|
+
overrides.together = {
|
|
209
|
+
enabled: true,
|
|
210
|
+
base_url: providers.together.baseUrl,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (providers.deepseek) {
|
|
214
|
+
overrides.deepseek = {
|
|
215
|
+
enabled: true,
|
|
216
|
+
base_url: providers.deepseek.baseUrl,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return overrides;
|
|
113
220
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Configure provider API keys from config
|
|
223
|
+
*/
|
|
224
|
+
configureProviderApiKeys() {
|
|
225
|
+
const providers = this.config.providers ?? {};
|
|
226
|
+
if (providers.anthropic?.apiKey) {
|
|
227
|
+
this.providerManager.setApiKey('anthropic', providers.anthropic.apiKey);
|
|
228
|
+
}
|
|
229
|
+
if (providers.openai?.apiKey) {
|
|
230
|
+
this.providerManager.setApiKey('openai', providers.openai.apiKey);
|
|
231
|
+
}
|
|
232
|
+
if (providers.openrouter?.apiKey) {
|
|
233
|
+
this.providerManager.setApiKey('openrouter', providers.openrouter.apiKey);
|
|
234
|
+
}
|
|
235
|
+
if (providers.google?.apiKey) {
|
|
236
|
+
this.providerManager.setApiKey('google', providers.google.apiKey);
|
|
237
|
+
}
|
|
238
|
+
if (providers.together?.apiKey) {
|
|
239
|
+
this.providerManager.setApiKey('together', providers.together.apiKey);
|
|
240
|
+
}
|
|
241
|
+
if (providers.deepseek?.apiKey) {
|
|
242
|
+
this.providerManager.setApiKey('deepseek', providers.deepseek.apiKey);
|
|
243
|
+
}
|
|
244
|
+
// Also try environment variables
|
|
245
|
+
const configs = this.capabilityRegistry.getEnabledProviders();
|
|
246
|
+
this.providerManager.setApiKeysFromEnv(configs);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Start the proxy server
|
|
250
|
+
*/
|
|
251
|
+
async start() {
|
|
252
|
+
// Initialize mesh learning layer (non-blocking, graceful degradation)
|
|
253
|
+
if (this.config.enableMesh && this.meshConfig.enabled) {
|
|
254
|
+
try {
|
|
255
|
+
this.meshHandle = await (0, mesh_js_1.initMesh)(this.meshConfig);
|
|
256
|
+
this.log('info', 'Mesh learning layer initialized');
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
this.log('error', `Mesh init failed (continuing without): ${err}`);
|
|
124
260
|
}
|
|
125
261
|
}
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
this.server = http.createServer((req, res) => {
|
|
264
|
+
this.handleRequest(req, res).catch((err) => {
|
|
265
|
+
this.log('error', `Unhandled error: ${err}`);
|
|
266
|
+
this.sendError(res, 500, 'internal_error', 'Internal server error');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
270
|
+
this.log('info', `RelayPlane Proxy listening on http://${this.config.host}:${this.config.port}`);
|
|
271
|
+
resolve();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
126
274
|
}
|
|
127
|
-
|
|
128
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Stop the proxy server
|
|
277
|
+
*/
|
|
278
|
+
async stop() {
|
|
279
|
+
// Stop mesh learning layer
|
|
280
|
+
if (this.meshHandle) {
|
|
281
|
+
try {
|
|
282
|
+
this.meshHandle.stop();
|
|
283
|
+
}
|
|
284
|
+
catch { /* ignore */ }
|
|
285
|
+
this.meshHandle = null;
|
|
286
|
+
}
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
if (this.server) {
|
|
289
|
+
this.server.close(() => {
|
|
290
|
+
this.log('info', 'Proxy server stopped');
|
|
291
|
+
resolve();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
resolve();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
129
298
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Handle incoming request
|
|
301
|
+
*/
|
|
302
|
+
async handleRequest(req, res) {
|
|
303
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
304
|
+
// CORS headers
|
|
305
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
306
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
307
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-RelayPlane-Workspace, X-RelayPlane-Agent, X-RelayPlane-Session, X-RelayPlane-Automated');
|
|
308
|
+
// Handle preflight
|
|
309
|
+
if (req.method === 'OPTIONS') {
|
|
310
|
+
res.writeHead(204);
|
|
311
|
+
res.end();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// Health check
|
|
315
|
+
if (url.pathname === '/health' || url.pathname === '/') {
|
|
316
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
317
|
+
res.end(JSON.stringify({ status: 'ok', version: '0.1.0' }));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// OpenAI-compatible chat completions
|
|
321
|
+
if (url.pathname === '/v1/chat/completions' && req.method === 'POST') {
|
|
322
|
+
await this.handleChatCompletions(req, res);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Models endpoint (for client compatibility)
|
|
326
|
+
if (url.pathname === '/v1/models' && req.method === 'GET') {
|
|
327
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
328
|
+
res.end(JSON.stringify({
|
|
329
|
+
object: 'list',
|
|
330
|
+
data: [
|
|
331
|
+
{ id: 'claude-3-5-sonnet', object: 'model', owned_by: 'anthropic' },
|
|
332
|
+
{ id: 'claude-3-5-haiku', object: 'model', owned_by: 'anthropic' },
|
|
333
|
+
{ id: 'gpt-4o', object: 'model', owned_by: 'openai' },
|
|
334
|
+
{ id: 'gpt-4o-mini', object: 'model', owned_by: 'openai' },
|
|
335
|
+
],
|
|
336
|
+
}));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Policy Management API (Phase 2)
|
|
340
|
+
if (url.pathname === '/v1/policies' && req.method === 'GET') {
|
|
341
|
+
await this.handleListPolicies(req, res);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (url.pathname === '/v1/policies' && req.method === 'POST') {
|
|
345
|
+
await this.handleCreatePolicy(req, res);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (url.pathname.startsWith('/v1/policies/') && req.method === 'GET') {
|
|
349
|
+
const policyId = url.pathname.split('/')[3];
|
|
350
|
+
if (policyId) {
|
|
351
|
+
await this.handleGetPolicy(res, policyId);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
140
354
|
}
|
|
141
|
-
|
|
355
|
+
if (url.pathname.startsWith('/v1/policies/') && req.method === 'PATCH') {
|
|
356
|
+
const policyId = url.pathname.split('/')[3];
|
|
357
|
+
if (policyId) {
|
|
358
|
+
await this.handleUpdatePolicy(req, res, policyId);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (url.pathname.startsWith('/v1/policies/') && req.method === 'DELETE') {
|
|
363
|
+
const policyId = url.pathname.split('/')[3];
|
|
364
|
+
if (policyId) {
|
|
365
|
+
await this.handleDeletePolicy(res, policyId);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (url.pathname === '/v1/policies/test' && req.method === 'POST') {
|
|
370
|
+
await this.handlePolicyTest(req, res);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Budget state endpoint
|
|
374
|
+
if (url.pathname === '/v1/budget' && req.method === 'GET') {
|
|
375
|
+
await this.handleGetBudget(req, res);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// ========================================================================
|
|
379
|
+
// Explainability API (Phase 4)
|
|
380
|
+
// ========================================================================
|
|
381
|
+
// GET /v1/runs/{id}/explain - Full decision chain explanation
|
|
382
|
+
if (url.pathname.match(/^\/v1\/runs\/[^/]+\/explain$/) && req.method === 'GET') {
|
|
383
|
+
const runId = url.pathname.split('/')[3];
|
|
384
|
+
const format = url.searchParams.get('format') ?? 'full';
|
|
385
|
+
await this.handleExplainRun(res, runId, format);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// GET /v1/runs/{id}/timeline - Timeline view only
|
|
389
|
+
if (url.pathname.match(/^\/v1\/runs\/[^/]+\/timeline$/) && req.method === 'GET') {
|
|
390
|
+
const runId = url.pathname.split('/')[3];
|
|
391
|
+
await this.handleRunTimeline(res, runId);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// GET /v1/runs/{id}/decisions - Raw decision chain
|
|
395
|
+
if (url.pathname.match(/^\/v1\/runs\/[^/]+\/decisions$/) && req.method === 'GET') {
|
|
396
|
+
const runId = url.pathname.split('/')[3];
|
|
397
|
+
await this.handleRunDecisions(res, runId);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// GET /v1/runs/{id} - Run inspector (all details)
|
|
401
|
+
if (url.pathname.match(/^\/v1\/runs\/[^/]+$/) && req.method === 'GET') {
|
|
402
|
+
const runId = url.pathname.split('/')[3];
|
|
403
|
+
await this.handleRunInspector(res, runId);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// GET /v1/runs/compare?ids=run1,run2 - Run comparison
|
|
407
|
+
if (url.pathname === '/v1/runs/compare' && req.method === 'GET') {
|
|
408
|
+
const idsParam = url.searchParams.get('ids');
|
|
409
|
+
const includeDecisions = url.searchParams.get('include_decisions') === 'true';
|
|
410
|
+
await this.handleCompareRuns(res, idsParam, includeDecisions);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// POST /v1/simulate/policy - Policy simulation
|
|
414
|
+
if (url.pathname === '/v1/simulate/policy' && req.method === 'POST') {
|
|
415
|
+
await this.handleSimulatePolicy(req, res);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// POST /v1/simulate/routing - Routing simulation
|
|
419
|
+
if (url.pathname === '/v1/simulate/routing' && req.method === 'POST') {
|
|
420
|
+
await this.handleSimulateRouting(req, res);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// ========================================================================
|
|
424
|
+
// Learning Engine API (Phase 5)
|
|
425
|
+
// ========================================================================
|
|
426
|
+
if (this.learningEngine) {
|
|
427
|
+
// GET /v1/analytics/summary
|
|
428
|
+
if (url.pathname === '/v1/analytics/summary' && req.method === 'GET') {
|
|
429
|
+
await this.handleAnalyticsSummary(req, res);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
// POST /v1/analytics/analyze
|
|
433
|
+
if (url.pathname === '/v1/analytics/analyze' && req.method === 'POST') {
|
|
434
|
+
await this.handleRunAnalysis(req, res);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// GET /v1/suggestions
|
|
438
|
+
if (url.pathname === '/v1/suggestions' && req.method === 'GET') {
|
|
439
|
+
await this.handleListSuggestions(req, res);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// POST /v1/suggestions/:id/approve
|
|
443
|
+
if (url.pathname.match(/^\/v1\/suggestions\/[^/]+\/approve$/) && req.method === 'POST') {
|
|
444
|
+
const id = url.pathname.split('/')[3];
|
|
445
|
+
await this.handleApproveSuggestion(req, res, id);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// POST /v1/suggestions/:id/reject
|
|
449
|
+
if (url.pathname.match(/^\/v1\/suggestions\/[^/]+\/reject$/) && req.method === 'POST') {
|
|
450
|
+
const id = url.pathname.split('/')[3];
|
|
451
|
+
await this.handleRejectSuggestion(req, res, id);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
// GET /v1/rules
|
|
455
|
+
if (url.pathname === '/v1/rules' && req.method === 'GET') {
|
|
456
|
+
await this.handleListRules(req, res);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
// GET /v1/rules/:id/effectiveness
|
|
460
|
+
if (url.pathname.match(/^\/v1\/rules\/[^/]+\/effectiveness$/) && req.method === 'GET') {
|
|
461
|
+
const id = url.pathname.split('/')[3];
|
|
462
|
+
await this.handleRuleEffectiveness(res, id);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// ========================================================================
|
|
467
|
+
// Mesh Learning Layer API (Phase 6)
|
|
468
|
+
// ========================================================================
|
|
469
|
+
if (url.pathname === '/v1/mesh/status' && req.method === 'GET') {
|
|
470
|
+
const status = this.meshHandle?.getStatus() ?? { available: false, enabled: false, atomCount: 0, contributing: false, meshUrl: '', dataDir: '' };
|
|
471
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
472
|
+
res.end(JSON.stringify({ mesh: status }));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (url.pathname === '/v1/mesh/tips' && req.method === 'GET') {
|
|
476
|
+
const tips = this.meshHandle?.getTips(20) ?? [];
|
|
477
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
478
|
+
res.end(JSON.stringify({ tips }));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (url.pathname === '/v1/mesh/sync' && req.method === 'POST') {
|
|
482
|
+
const result = await (this.meshHandle?.sync() ?? Promise.resolve(null));
|
|
483
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
484
|
+
res.end(JSON.stringify({ sync: result ?? { error: 'Mesh not available or not contributing' } }));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// 404 for unknown routes
|
|
488
|
+
this.sendError(res, 404, 'not_found', `Unknown endpoint: ${url.pathname}`);
|
|
142
489
|
}
|
|
143
|
-
|
|
144
|
-
|
|
490
|
+
// ============================================================================
|
|
491
|
+
// Policy Management Handlers (Phase 2)
|
|
492
|
+
// ============================================================================
|
|
493
|
+
async handleListPolicies(req, res) {
|
|
494
|
+
try {
|
|
495
|
+
const workspaceId = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
496
|
+
const policies = await this.policyEngine.listPolicies(workspaceId);
|
|
497
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
498
|
+
res.end(JSON.stringify({ policies }));
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
502
|
+
}
|
|
145
503
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
504
|
+
async handleCreatePolicy(req, res) {
|
|
505
|
+
try {
|
|
506
|
+
const body = await this.readBody(req);
|
|
507
|
+
const policy = JSON.parse(body);
|
|
508
|
+
const policyId = await this.policyEngine.createPolicy(policy);
|
|
509
|
+
const created = await this.policyEngine.getPolicy(policyId);
|
|
510
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
511
|
+
res.end(JSON.stringify({ policy: created }));
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
this.sendError(res, 400, 'invalid_request', err instanceof Error ? err.message : 'Invalid policy');
|
|
156
515
|
}
|
|
157
516
|
}
|
|
158
|
-
|
|
159
|
-
|
|
517
|
+
async handleGetPolicy(res, policyId) {
|
|
518
|
+
try {
|
|
519
|
+
const policy = await this.policyEngine.getPolicy(policyId);
|
|
520
|
+
if (!policy) {
|
|
521
|
+
this.sendError(res, 404, 'not_found', 'Policy not found');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
525
|
+
res.end(JSON.stringify({ policy }));
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
529
|
+
}
|
|
160
530
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
531
|
+
async handleUpdatePolicy(req, res, policyId) {
|
|
532
|
+
try {
|
|
533
|
+
const body = await this.readBody(req);
|
|
534
|
+
const updates = JSON.parse(body);
|
|
535
|
+
await this.policyEngine.updatePolicy(policyId, updates);
|
|
536
|
+
const updated = await this.policyEngine.getPolicy(policyId);
|
|
537
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
538
|
+
res.end(JSON.stringify({ policy: updated }));
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
this.sendError(res, 400, 'invalid_request', err instanceof Error ? err.message : 'Invalid update');
|
|
168
542
|
}
|
|
169
|
-
fs.writeFileSync(usagePath, JSON.stringify(usage, null, 2));
|
|
170
543
|
}
|
|
171
|
-
|
|
172
|
-
|
|
544
|
+
async handleDeletePolicy(res, policyId) {
|
|
545
|
+
try {
|
|
546
|
+
await this.policyEngine.deletePolicy(policyId);
|
|
547
|
+
res.writeHead(204);
|
|
548
|
+
res.end();
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
552
|
+
}
|
|
173
553
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
console.warn(`⚠️ DAILY LIMIT REACHED: $${dailyUsage.cost.toFixed(2)} / $${config.limits.daily} (100%)`);
|
|
185
|
-
loggedWarnings.daily100 = true;
|
|
186
|
-
warnings.push(`Daily limit reached: $${dailyUsage.cost.toFixed(2)} of $${config.limits.daily}`);
|
|
187
|
-
}
|
|
188
|
-
else if (dailyPercent >= 90 && !loggedWarnings.daily90) {
|
|
189
|
-
console.warn(`⚠️ Daily spending at 90%: $${dailyUsage.cost.toFixed(2)} / $${config.limits.daily}`);
|
|
190
|
-
loggedWarnings.daily90 = true;
|
|
191
|
-
warnings.push(`⚠️ You've used $${dailyUsage.cost.toFixed(2)} of your $${config.limits.daily} daily limit`);
|
|
192
|
-
}
|
|
193
|
-
else if (dailyPercent >= 80 && !loggedWarnings.daily80) {
|
|
194
|
-
console.warn(`⚠️ Daily spending at 80%: $${dailyUsage.cost.toFixed(2)} / $${config.limits.daily}`);
|
|
195
|
-
loggedWarnings.daily80 = true;
|
|
196
|
-
warnings.push(`⚠️ You've used $${dailyUsage.cost.toFixed(2)} of your $${config.limits.daily} daily limit`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Check monthly limits
|
|
200
|
-
if (config?.limits?.monthly) {
|
|
201
|
-
const monthlyPercent = (monthlyUsage.cost / config.limits.monthly) * 100;
|
|
202
|
-
if (monthlyPercent >= 100 && !loggedWarnings.monthly100) {
|
|
203
|
-
console.warn(`⚠️ MONTHLY LIMIT REACHED: $${monthlyUsage.cost.toFixed(2)} / $${config.limits.monthly} (100%)`);
|
|
204
|
-
loggedWarnings.monthly100 = true;
|
|
205
|
-
warnings.push(`Monthly limit reached: $${monthlyUsage.cost.toFixed(2)} of $${config.limits.monthly}`);
|
|
206
|
-
}
|
|
207
|
-
else if (monthlyPercent >= 90 && !loggedWarnings.monthly90) {
|
|
208
|
-
console.warn(`⚠️ Monthly spending at 90%: $${monthlyUsage.cost.toFixed(2)} / $${config.limits.monthly}`);
|
|
209
|
-
loggedWarnings.monthly90 = true;
|
|
210
|
-
warnings.push(`⚠️ You've used $${monthlyUsage.cost.toFixed(2)} of your $${config.limits.monthly} monthly limit`);
|
|
211
|
-
}
|
|
212
|
-
else if (monthlyPercent >= 80 && !loggedWarnings.monthly80) {
|
|
213
|
-
console.warn(`⚠️ Monthly spending at 80%: $${monthlyUsage.cost.toFixed(2)} / $${config.limits.monthly}`);
|
|
214
|
-
loggedWarnings.monthly80 = true;
|
|
215
|
-
warnings.push(`⚠️ You've used $${monthlyUsage.cost.toFixed(2)} of your $${config.limits.monthly} monthly limit`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return warnings.length > 0 ? warnings.join('; ') : null;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Log usage to JSONL file
|
|
222
|
-
*/
|
|
223
|
-
function logUsage(record) {
|
|
224
|
-
const usagePath = path.join(CONFIG_DIR, 'usage.jsonl');
|
|
225
|
-
try {
|
|
226
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
227
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
554
|
+
async handlePolicyTest(req, res) {
|
|
555
|
+
try {
|
|
556
|
+
const body = await this.readBody(req);
|
|
557
|
+
const testRequest = JSON.parse(body);
|
|
558
|
+
const decision = await this.policyEngine.dryRun(testRequest);
|
|
559
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
560
|
+
res.end(JSON.stringify({ decision }));
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
this.sendError(res, 400, 'invalid_request', err instanceof Error ? err.message : 'Invalid test request');
|
|
228
564
|
}
|
|
229
|
-
fs.appendFileSync(usagePath, JSON.stringify(record) + '\n');
|
|
230
565
|
}
|
|
231
|
-
|
|
232
|
-
|
|
566
|
+
async handleGetBudget(req, res) {
|
|
567
|
+
try {
|
|
568
|
+
const workspaceId = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
569
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
570
|
+
const scopeType = (url.searchParams.get('scope_type') ?? 'workspace');
|
|
571
|
+
const scopeId = url.searchParams.get('scope_id') ?? workspaceId;
|
|
572
|
+
const period = (url.searchParams.get('period') ?? 'day');
|
|
573
|
+
const state = await this.policyEngine.getBudgetState(workspaceId, scopeType, scopeId, period);
|
|
574
|
+
if (!state) {
|
|
575
|
+
// Return empty state if no budget configured
|
|
576
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
577
|
+
res.end(JSON.stringify({ budget_state: null, message: 'No budget state found' }));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
581
|
+
res.end(JSON.stringify({ budget_state: state }));
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
585
|
+
}
|
|
233
586
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
let body = '';
|
|
261
|
-
req.on('data', chunk => (body += chunk.toString()));
|
|
262
|
-
req.on('end', () => resolve(body));
|
|
263
|
-
req.on('error', reject);
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Send JSON response
|
|
268
|
-
*/
|
|
269
|
-
function sendJson(res, status, data) {
|
|
270
|
-
res.writeHead(status, {
|
|
271
|
-
'Content-Type': 'application/json',
|
|
272
|
-
'Access-Control-Allow-Origin': '*',
|
|
273
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
274
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Dry-Run',
|
|
275
|
-
});
|
|
276
|
-
res.end(JSON.stringify(data));
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Get provider configuration status
|
|
280
|
-
*/
|
|
281
|
-
function getProviderStatus() {
|
|
282
|
-
const providerStatus = {};
|
|
283
|
-
for (const [name, config] of Object.entries(PROVIDERS)) {
|
|
284
|
-
const apiKey = process.env[config.apiKeyEnv];
|
|
285
|
-
providerStatus[name] = apiKey ? 'configured' : 'not_configured';
|
|
587
|
+
// ============================================================================
|
|
588
|
+
// Explainability Handlers (Phase 4)
|
|
589
|
+
// ============================================================================
|
|
590
|
+
/**
|
|
591
|
+
* Handle GET /v1/runs/{id}/explain - Full decision chain explanation
|
|
592
|
+
*/
|
|
593
|
+
async handleExplainRun(res, runId, format) {
|
|
594
|
+
try {
|
|
595
|
+
const explanation = await this.explainer.explain(runId, format);
|
|
596
|
+
if (!explanation) {
|
|
597
|
+
this.sendError(res, 404, 'not_found', `Run not found: ${runId}`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
601
|
+
res.end(JSON.stringify({
|
|
602
|
+
run_id: runId,
|
|
603
|
+
format,
|
|
604
|
+
chain: explanation.chain,
|
|
605
|
+
timeline: explanation.timeline,
|
|
606
|
+
narrative: explanation.narrative,
|
|
607
|
+
debug_info: explanation.debug_info,
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
612
|
+
}
|
|
286
613
|
}
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
*/
|
|
292
|
-
function handleHealth(res) {
|
|
293
|
-
const config = loadConfig();
|
|
294
|
-
const dailyUsage = loadDailyUsage();
|
|
295
|
-
const monthlyUsage = loadMonthlyUsage();
|
|
296
|
-
const health = {
|
|
297
|
-
status: 'ok',
|
|
298
|
-
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
299
|
-
version: VERSION,
|
|
300
|
-
providers: getProviderStatus(),
|
|
301
|
-
requestsHandled: stats.requestsHandled,
|
|
302
|
-
requestsSuccessful: stats.requestsSuccessful,
|
|
303
|
-
requestsFailed: stats.requestsFailed,
|
|
304
|
-
dailyCost: dailyUsage.cost,
|
|
305
|
-
dailyLimit: config?.limits?.daily,
|
|
306
|
-
monthlyCost: monthlyUsage.cost,
|
|
307
|
-
monthlyLimit: config?.limits?.monthly,
|
|
308
|
-
usage: {
|
|
309
|
-
inputTokens: stats.totalInputTokens,
|
|
310
|
-
outputTokens: stats.totalOutputTokens,
|
|
311
|
-
totalCost: stats.totalCost,
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
sendJson(res, 200, health);
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Handle /v1/models endpoint
|
|
318
|
-
*/
|
|
319
|
-
function handleModels(res) {
|
|
320
|
-
const models = [
|
|
321
|
-
// OpenAI
|
|
322
|
-
{ id: 'gpt-4o', provider: 'openai', alias: null },
|
|
323
|
-
{ id: 'gpt-4o-mini', provider: 'openai', alias: 'rp:balanced' },
|
|
324
|
-
{ id: 'gpt-4-turbo', provider: 'openai', alias: null },
|
|
325
|
-
{ id: 'gpt-3.5-turbo', provider: 'openai', alias: null },
|
|
326
|
-
// Anthropic
|
|
327
|
-
{ id: 'claude-3-5-sonnet-20241022', provider: 'anthropic', alias: 'rp:best' },
|
|
328
|
-
{ id: 'claude-3-opus-20240229', provider: 'anthropic', alias: null },
|
|
329
|
-
{ id: 'claude-3-haiku-20240307', provider: 'anthropic', alias: null },
|
|
330
|
-
// Groq
|
|
331
|
-
{ id: 'llama-3.1-70b-versatile', provider: 'groq', alias: null },
|
|
332
|
-
{ id: 'llama-3.1-8b-instant', provider: 'groq', alias: 'rp:fast, rp:cheap' },
|
|
333
|
-
{ id: 'mixtral-8x7b-32768', provider: 'groq', alias: null },
|
|
334
|
-
// Aliases
|
|
335
|
-
{ id: 'rp:fast', provider: 'groq', alias: '→ llama-3.1-8b-instant' },
|
|
336
|
-
{ id: 'rp:cheap', provider: 'groq', alias: '→ llama-3.1-8b-instant' },
|
|
337
|
-
{ id: 'rp:best', provider: 'anthropic', alias: '→ claude-3-5-sonnet-20241022' },
|
|
338
|
-
{ id: 'rp:balanced', provider: 'openai', alias: '→ gpt-4o-mini' },
|
|
339
|
-
];
|
|
340
|
-
sendJson(res, 200, {
|
|
341
|
-
object: 'list',
|
|
342
|
-
data: models.map(m => ({
|
|
343
|
-
id: m.id,
|
|
344
|
-
object: 'model',
|
|
345
|
-
owned_by: m.provider,
|
|
346
|
-
relayplane_alias: m.alias,
|
|
347
|
-
})),
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Handle chat completions (and other API endpoints)
|
|
352
|
-
*/
|
|
353
|
-
async function handleProxy(req, res, pathname) {
|
|
354
|
-
const startMs = Date.now();
|
|
355
|
-
const isDryRun = req.headers['x-dry-run'] === 'true';
|
|
356
|
-
try {
|
|
357
|
-
const body = await parseBody(req);
|
|
358
|
-
let data;
|
|
614
|
+
/**
|
|
615
|
+
* Handle GET /v1/runs/{id}/timeline - Timeline view only
|
|
616
|
+
*/
|
|
617
|
+
async handleRunTimeline(res, runId) {
|
|
359
618
|
try {
|
|
360
|
-
|
|
619
|
+
const timeline = await this.explainer.getTimeline(runId);
|
|
620
|
+
if (timeline.length === 0) {
|
|
621
|
+
this.sendError(res, 404, 'not_found', `Run not found: ${runId}`);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
625
|
+
res.end(JSON.stringify({ run_id: runId, timeline }));
|
|
361
626
|
}
|
|
362
|
-
catch {
|
|
363
|
-
|
|
364
|
-
return;
|
|
627
|
+
catch (err) {
|
|
628
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
365
629
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Handle GET /v1/runs/{id}/decisions - Raw decision chain
|
|
633
|
+
*/
|
|
634
|
+
async handleRunDecisions(res, runId) {
|
|
635
|
+
try {
|
|
636
|
+
const chain = await this.explainer.getDecisionChain(runId);
|
|
637
|
+
if (!chain) {
|
|
638
|
+
this.sendError(res, 404, 'not_found', `Run not found: ${runId}`);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
642
|
+
res.end(JSON.stringify({
|
|
643
|
+
run_id: runId,
|
|
644
|
+
decisions: chain.decisions,
|
|
645
|
+
summary: chain.summary,
|
|
646
|
+
insights: chain.insights,
|
|
647
|
+
}));
|
|
374
648
|
}
|
|
375
|
-
|
|
376
|
-
|
|
649
|
+
catch (err) {
|
|
650
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
377
651
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Handle GET /v1/runs/{id} - Run inspector (all details)
|
|
655
|
+
*/
|
|
656
|
+
async handleRunInspector(res, runId) {
|
|
657
|
+
try {
|
|
658
|
+
// Get run from ledger
|
|
659
|
+
const run = await this.ledger.getRun(runId);
|
|
660
|
+
if (!run) {
|
|
661
|
+
this.sendError(res, 404, 'not_found', `Run not found: ${runId}`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// Get events
|
|
665
|
+
const events = await this.ledger.getRunEvents(runId);
|
|
666
|
+
// Get decision chain
|
|
667
|
+
const chain = await this.explainer.getDecisionChain(runId);
|
|
668
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
669
|
+
res.end(JSON.stringify({
|
|
670
|
+
run,
|
|
671
|
+
events,
|
|
672
|
+
decision_chain: chain,
|
|
673
|
+
}));
|
|
382
674
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
675
|
+
catch (err) {
|
|
676
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Handle GET /v1/runs/compare?ids=run1,run2 - Run comparison
|
|
681
|
+
*/
|
|
682
|
+
async handleCompareRuns(res, idsParam, includeDecisions) {
|
|
683
|
+
try {
|
|
684
|
+
if (!idsParam) {
|
|
685
|
+
this.sendError(res, 400, 'invalid_request', 'Missing required parameter: ids');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const runIds = idsParam.split(',').map(id => id.trim()).filter(Boolean);
|
|
689
|
+
if (runIds.length < 2) {
|
|
690
|
+
this.sendError(res, 400, 'invalid_request', 'At least 2 run IDs required for comparison');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const comparison = await this.comparator.compare(runIds, {
|
|
694
|
+
includeDecisionDiff: includeDecisions,
|
|
387
695
|
});
|
|
388
|
-
|
|
696
|
+
if (!comparison) {
|
|
697
|
+
this.sendError(res, 404, 'not_found', 'One or more runs not found');
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
701
|
+
res.end(JSON.stringify(comparison));
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
389
705
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Handle POST /v1/simulate/policy - Policy simulation
|
|
709
|
+
*/
|
|
710
|
+
async handleSimulatePolicy(req, res) {
|
|
711
|
+
try {
|
|
712
|
+
const body = await this.readBody(req);
|
|
713
|
+
const request = JSON.parse(body);
|
|
714
|
+
// Use workspace from header if not in body
|
|
715
|
+
if (!request.workspace_id) {
|
|
716
|
+
request.workspace_id = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
717
|
+
}
|
|
718
|
+
const result = await this.simulator.simulatePolicy(request);
|
|
719
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
720
|
+
res.end(JSON.stringify(result));
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
this.sendError(res, 400, 'invalid_request', err instanceof Error ? err.message : 'Invalid simulation request');
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Handle POST /v1/simulate/routing - Routing simulation
|
|
728
|
+
*/
|
|
729
|
+
async handleSimulateRouting(req, res) {
|
|
730
|
+
try {
|
|
731
|
+
const body = await this.readBody(req);
|
|
732
|
+
const request = JSON.parse(body);
|
|
733
|
+
// Use workspace from header if not in body
|
|
734
|
+
if (!request.workspace_id) {
|
|
735
|
+
request.workspace_id = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
736
|
+
}
|
|
737
|
+
const result = await this.simulator.simulateRouting(request);
|
|
738
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
739
|
+
res.end(JSON.stringify(result));
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
this.sendError(res, 400, 'invalid_request', err instanceof Error ? err.message : 'Invalid simulation request');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Handle /v1/chat/completions
|
|
747
|
+
*/
|
|
748
|
+
async handleChatCompletions(req, res) {
|
|
749
|
+
const startTime = Date.now();
|
|
750
|
+
let runId = null;
|
|
751
|
+
try {
|
|
752
|
+
// Parse request body
|
|
753
|
+
const body = await this.readBody(req);
|
|
754
|
+
const request = JSON.parse(body);
|
|
755
|
+
// Extract metadata from headers
|
|
756
|
+
const workspaceId = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
757
|
+
const agentId = req.headers['x-relayplane-agent'] ?? this.config.defaultAgentId;
|
|
758
|
+
const sessionId = req.headers['x-relayplane-session'];
|
|
759
|
+
const isAutomated = req.headers['x-relayplane-automated'] === 'true';
|
|
760
|
+
// Determine provider
|
|
761
|
+
const provider = getProviderForModel(request.model);
|
|
762
|
+
// Detect auth type from Authorization header
|
|
763
|
+
const authHeader = req.headers['authorization'];
|
|
764
|
+
const authType = this.detectAuthType(authHeader);
|
|
765
|
+
const executionMode = isAutomated ? 'background' : 'interactive';
|
|
766
|
+
// Validate auth via Auth Gate
|
|
767
|
+
const authResult = await this.authGate.validate({
|
|
768
|
+
workspace_id: workspaceId,
|
|
769
|
+
metadata: {
|
|
770
|
+
session_type: isAutomated ? 'background' : 'interactive',
|
|
771
|
+
headers: {
|
|
772
|
+
'X-RelayPlane-Automated': isAutomated ? 'true' : 'false',
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
// Start ledger run
|
|
777
|
+
runId = await this.ledger.startRun({
|
|
778
|
+
workspace_id: workspaceId,
|
|
779
|
+
agent_id: agentId,
|
|
780
|
+
session_id: sessionId,
|
|
781
|
+
provider,
|
|
782
|
+
model: request.model,
|
|
783
|
+
auth_type: authType,
|
|
784
|
+
execution_mode: executionMode,
|
|
785
|
+
compliance_mode: this.config.defaultAuthEnforcementMode,
|
|
786
|
+
auth_risk: authResult.ledger_flags.auth_risk,
|
|
787
|
+
policy_override: authResult.ledger_flags.policy_override,
|
|
788
|
+
});
|
|
789
|
+
// Record auth validation
|
|
790
|
+
await this.authGate.emitAuthEvent(runId, authResult);
|
|
791
|
+
// Check if auth was denied
|
|
792
|
+
if (!authResult.allow) {
|
|
793
|
+
const latencyMs = Date.now() - startTime;
|
|
794
|
+
await this.ledger.completeRun(runId, {
|
|
795
|
+
status: 'failed',
|
|
796
|
+
input_tokens: 0,
|
|
797
|
+
output_tokens: 0,
|
|
798
|
+
total_tokens: 0,
|
|
799
|
+
cost_usd: 0,
|
|
800
|
+
latency_ms: latencyMs,
|
|
401
801
|
error: {
|
|
402
|
-
|
|
403
|
-
|
|
802
|
+
code: 'auth_denied',
|
|
803
|
+
message: authResult.reason ?? 'Authentication denied',
|
|
804
|
+
retryable: false,
|
|
404
805
|
},
|
|
405
806
|
});
|
|
807
|
+
this.sendError(res, 403, 'auth_denied', authResult.reason ?? 'Authentication denied', runId, authResult.guidance_url);
|
|
406
808
|
return;
|
|
407
809
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
810
|
+
// Evaluate policies (Phase 2)
|
|
811
|
+
if (this.config.enforcePolicies) {
|
|
812
|
+
const estimatedCost = this.policyEngine.estimateCost(request.model, provider, request.messages?.reduce((sum, m) => sum + (m.content?.length ?? 0) / 4, 0) ?? 1000, // Rough token estimate
|
|
813
|
+
request.max_tokens ?? 1000);
|
|
814
|
+
const policyDecision = await this.policyEngine.evaluate({
|
|
815
|
+
workspace_id: workspaceId,
|
|
816
|
+
agent_id: agentId,
|
|
817
|
+
session_id: sessionId,
|
|
818
|
+
run_id: runId,
|
|
819
|
+
request: {
|
|
820
|
+
model: request.model,
|
|
821
|
+
provider,
|
|
822
|
+
estimated_cost_usd: estimatedCost,
|
|
823
|
+
estimated_tokens: request.max_tokens,
|
|
824
|
+
context_size: request.messages?.reduce((sum, m) => sum + (m.content?.length ?? 0), 0),
|
|
825
|
+
tools_requested: request.tools?.map((t) => t.function?.name).filter((n) => !!n),
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
// Record policy evaluation in ledger
|
|
829
|
+
await this.ledger.recordPolicyEvaluation(runId, policyDecision.policies_evaluated.map((p) => ({
|
|
830
|
+
policy_id: p.policy_id,
|
|
831
|
+
policy_name: p.policy_name,
|
|
832
|
+
matched: p.matched,
|
|
833
|
+
action_taken: p.action_taken,
|
|
834
|
+
})));
|
|
835
|
+
// Check if policy denied the request
|
|
836
|
+
if (!policyDecision.allow) {
|
|
837
|
+
const latencyMs = Date.now() - startTime;
|
|
838
|
+
await this.ledger.completeRun(runId, {
|
|
839
|
+
status: 'failed',
|
|
840
|
+
input_tokens: 0,
|
|
841
|
+
output_tokens: 0,
|
|
842
|
+
total_tokens: 0,
|
|
843
|
+
cost_usd: 0,
|
|
844
|
+
latency_ms: latencyMs,
|
|
845
|
+
error: {
|
|
846
|
+
code: policyDecision.approval_required ? 'approval_required' : 'policy_denied',
|
|
847
|
+
message: policyDecision.reason ?? 'Policy denied the request',
|
|
848
|
+
retryable: false,
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
if (policyDecision.approval_required) {
|
|
852
|
+
this.sendError(res, 403, 'approval_required', policyDecision.reason ?? 'Approval required', runId);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
this.sendError(res, 403, 'policy_denied', policyDecision.reason ?? 'Policy denied the request', runId);
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
// Apply any modifications from policy (e.g., model downgrade, context cap)
|
|
860
|
+
if (policyDecision.modified_request) {
|
|
861
|
+
if (policyDecision.modified_request.model) {
|
|
862
|
+
request.model = policyDecision.modified_request.model;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// Log budget warning if present
|
|
866
|
+
if (policyDecision.budget_warning) {
|
|
867
|
+
this.log('info', `Budget warning for ${workspaceId}: ${policyDecision.budget_warning}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// Record routing decision
|
|
871
|
+
await this.ledger.recordRouting(runId, {
|
|
872
|
+
selected_provider: provider,
|
|
873
|
+
selected_model: request.model,
|
|
874
|
+
reason: 'Direct model selection by client',
|
|
875
|
+
});
|
|
876
|
+
// Forward to provider
|
|
877
|
+
const providerConfig = this.config.providers?.[provider];
|
|
878
|
+
if (!providerConfig?.apiKey) {
|
|
879
|
+
const latencyMs = Date.now() - startTime;
|
|
880
|
+
await this.ledger.completeRun(runId, {
|
|
881
|
+
status: 'failed',
|
|
882
|
+
input_tokens: 0,
|
|
883
|
+
output_tokens: 0,
|
|
884
|
+
total_tokens: 0,
|
|
885
|
+
cost_usd: 0,
|
|
886
|
+
latency_ms: latencyMs,
|
|
412
887
|
error: {
|
|
413
|
-
|
|
414
|
-
|
|
888
|
+
code: 'provider_not_configured',
|
|
889
|
+
message: `Provider ${provider} is not configured`,
|
|
890
|
+
retryable: false,
|
|
415
891
|
},
|
|
416
892
|
});
|
|
893
|
+
this.sendError(res, 500, 'provider_not_configured', `Provider ${provider} is not configured`, runId);
|
|
417
894
|
return;
|
|
418
895
|
}
|
|
896
|
+
// Make provider request
|
|
897
|
+
const providerResponse = await this.forwardToProvider(provider, request, providerConfig, runId);
|
|
898
|
+
const latencyMs = Date.now() - startTime;
|
|
899
|
+
if (providerResponse.success) {
|
|
900
|
+
const costUsd = this.estimateCost(provider, providerResponse.usage);
|
|
901
|
+
// Complete run successfully
|
|
902
|
+
await this.ledger.completeRun(runId, {
|
|
903
|
+
status: 'completed',
|
|
904
|
+
input_tokens: providerResponse.usage?.prompt_tokens ?? 0,
|
|
905
|
+
output_tokens: providerResponse.usage?.completion_tokens ?? 0,
|
|
906
|
+
total_tokens: providerResponse.usage?.total_tokens ?? 0,
|
|
907
|
+
cost_usd: costUsd,
|
|
908
|
+
latency_ms: latencyMs,
|
|
909
|
+
ttft_ms: providerResponse.ttft_ms,
|
|
910
|
+
});
|
|
911
|
+
// Record spend for budget tracking (Phase 2)
|
|
912
|
+
if (this.config.enforcePolicies) {
|
|
913
|
+
await this.policyEngine.recordSpend(workspaceId, agentId, runId, costUsd);
|
|
914
|
+
}
|
|
915
|
+
// Capture to mesh learning layer (Phase 6)
|
|
916
|
+
if (this.meshHandle) {
|
|
917
|
+
this.meshHandle.captureRequest({
|
|
918
|
+
toolName: 'chat_completion',
|
|
919
|
+
model: request.model,
|
|
920
|
+
provider,
|
|
921
|
+
success: true,
|
|
922
|
+
costUsd,
|
|
923
|
+
latencyMs,
|
|
924
|
+
inputTokens: providerResponse.usage?.prompt_tokens ?? 0,
|
|
925
|
+
outputTokens: providerResponse.usage?.completion_tokens ?? 0,
|
|
926
|
+
taskType: request.tools?.length ? 'tool_use' : 'chat',
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
// Add run_id to response
|
|
930
|
+
const responseData = providerResponse.data;
|
|
931
|
+
const responseWithMeta = {
|
|
932
|
+
...responseData,
|
|
933
|
+
relayplane: {
|
|
934
|
+
run_id: runId,
|
|
935
|
+
latency_ms: latencyMs,
|
|
936
|
+
ttft_ms: providerResponse.ttft_ms,
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
940
|
+
res.end(JSON.stringify(responseWithMeta));
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
// Complete run with failure
|
|
944
|
+
await this.ledger.completeRun(runId, {
|
|
945
|
+
status: 'failed',
|
|
946
|
+
input_tokens: 0,
|
|
947
|
+
output_tokens: 0,
|
|
948
|
+
total_tokens: 0,
|
|
949
|
+
cost_usd: 0,
|
|
950
|
+
latency_ms: latencyMs,
|
|
951
|
+
error: {
|
|
952
|
+
code: providerResponse.error?.code ?? 'provider_error',
|
|
953
|
+
message: providerResponse.error?.message ?? 'Provider request failed',
|
|
954
|
+
provider_error: providerResponse.error?.raw,
|
|
955
|
+
retryable: providerResponse.error?.retryable ?? false,
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
// Capture failed request to mesh (Phase 6)
|
|
959
|
+
if (this.meshHandle) {
|
|
960
|
+
this.meshHandle.captureRequest({
|
|
961
|
+
toolName: 'chat_completion',
|
|
962
|
+
model: request.model,
|
|
963
|
+
provider,
|
|
964
|
+
success: false,
|
|
965
|
+
costUsd: 0,
|
|
966
|
+
latencyMs,
|
|
967
|
+
inputTokens: 0,
|
|
968
|
+
outputTokens: 0,
|
|
969
|
+
taskType: request.tools?.length ? 'tool_use' : 'chat',
|
|
970
|
+
errorCode: providerResponse.error?.code,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
this.sendError(res, providerResponse.error?.status ?? 500, providerResponse.error?.code ?? 'provider_error', providerResponse.error?.message ?? 'Provider request failed', runId);
|
|
974
|
+
}
|
|
419
975
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
976
|
+
catch (err) {
|
|
977
|
+
const latencyMs = Date.now() - startTime;
|
|
978
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
979
|
+
if (runId) {
|
|
980
|
+
await this.ledger.completeRun(runId, {
|
|
981
|
+
status: 'failed',
|
|
982
|
+
input_tokens: 0,
|
|
983
|
+
output_tokens: 0,
|
|
984
|
+
total_tokens: 0,
|
|
985
|
+
cost_usd: 0,
|
|
986
|
+
latency_ms: latencyMs,
|
|
987
|
+
error: {
|
|
988
|
+
code: 'internal_error',
|
|
989
|
+
message: errorMessage,
|
|
990
|
+
retryable: false,
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
this.sendError(res, 500, 'internal_error', errorMessage, runId ?? undefined);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Forward request to provider
|
|
999
|
+
*/
|
|
1000
|
+
async forwardToProvider(provider, request, config, runId) {
|
|
1001
|
+
const endpoint = PROVIDER_ENDPOINTS[provider];
|
|
1002
|
+
if (!endpoint) {
|
|
1003
|
+
return {
|
|
1004
|
+
success: false,
|
|
1005
|
+
error: {
|
|
1006
|
+
code: 'unknown_provider',
|
|
1007
|
+
message: `Unknown provider: ${provider}`,
|
|
1008
|
+
status: 500,
|
|
1009
|
+
retryable: false,
|
|
446
1010
|
},
|
|
447
|
-
}
|
|
448
|
-
return;
|
|
1011
|
+
};
|
|
449
1012
|
}
|
|
450
|
-
|
|
451
|
-
const url =
|
|
1013
|
+
const baseUrl = config.baseUrl ?? endpoint.baseUrl;
|
|
1014
|
+
const url = `${baseUrl}/chat/completions`;
|
|
452
1015
|
const headers = {
|
|
453
1016
|
'Content-Type': 'application/json',
|
|
454
|
-
'User-Agent': `relayplane-proxy/${VERSION}`,
|
|
455
|
-
[providerConfig.headerName]: providerConfig.headerPrefix + apiKey,
|
|
456
|
-
...providerConfig.extraHeaders,
|
|
457
1017
|
};
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
headers
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1018
|
+
// Set auth header
|
|
1019
|
+
if (provider === 'anthropic') {
|
|
1020
|
+
headers['x-api-key'] = config.apiKey;
|
|
1021
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
const ttftStart = Date.now();
|
|
1028
|
+
const response = await fetch(url, {
|
|
1029
|
+
method: 'POST',
|
|
1030
|
+
headers,
|
|
1031
|
+
body: JSON.stringify(request),
|
|
1032
|
+
});
|
|
1033
|
+
// Record provider call
|
|
1034
|
+
await this.ledger.recordProviderCall(runId, {
|
|
1035
|
+
provider,
|
|
1036
|
+
model: request.model,
|
|
1037
|
+
attempt: 1,
|
|
1038
|
+
ttft_ms: Date.now() - ttftStart,
|
|
1039
|
+
});
|
|
1040
|
+
if (!response.ok) {
|
|
1041
|
+
const errorBody = await response.text();
|
|
1042
|
+
let parsedError;
|
|
470
1043
|
try {
|
|
471
|
-
|
|
472
|
-
if (respData.usage) {
|
|
473
|
-
outputTokens = respData.usage.completion_tokens || outputTokens;
|
|
474
|
-
}
|
|
1044
|
+
parsedError = JSON.parse(errorBody);
|
|
475
1045
|
}
|
|
476
1046
|
catch {
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
const actualCost = estimateCost(model, inputTokens, outputTokens);
|
|
480
|
-
// Update stats
|
|
481
|
-
stats.requestsHandled++;
|
|
482
|
-
if (success) {
|
|
483
|
-
stats.requestsSuccessful++;
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
stats.requestsFailed++;
|
|
1047
|
+
parsedError = errorBody;
|
|
487
1048
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// Update provider stats
|
|
498
|
-
const providerStats = stats.byProvider.get(provider) || { requests: 0, tokens: 0, cost: 0 };
|
|
499
|
-
providerStats.requests++;
|
|
500
|
-
providerStats.tokens += inputTokens + outputTokens;
|
|
501
|
-
providerStats.cost += actualCost;
|
|
502
|
-
stats.byProvider.set(provider, providerStats);
|
|
503
|
-
// Update daily usage
|
|
504
|
-
dailyUsage.cost += actualCost;
|
|
505
|
-
dailyUsage.requests++;
|
|
506
|
-
saveDailyUsage(dailyUsage);
|
|
507
|
-
// Update monthly usage
|
|
508
|
-
monthlyUsage.cost += actualCost;
|
|
509
|
-
monthlyUsage.requests++;
|
|
510
|
-
saveMonthlyUsage(monthlyUsage);
|
|
511
|
-
// Log usage
|
|
512
|
-
logUsage({
|
|
513
|
-
timestamp: new Date().toISOString(),
|
|
514
|
-
model,
|
|
515
|
-
provider,
|
|
516
|
-
inputTokens,
|
|
517
|
-
outputTokens,
|
|
518
|
-
cost: actualCost,
|
|
519
|
-
latencyMs,
|
|
520
|
-
success,
|
|
521
|
-
});
|
|
522
|
-
// Check limits and log warnings to console
|
|
523
|
-
const usageWarning = checkAndWarnLimits(dailyUsage, monthlyUsage, config);
|
|
524
|
-
// Add usage warning header if approaching limit
|
|
525
|
-
const responseHeaders = {
|
|
526
|
-
'Content-Type': 'application/json',
|
|
527
|
-
'Access-Control-Allow-Origin': '*',
|
|
528
|
-
'X-RelayPlane-Cost': actualCost.toFixed(6),
|
|
529
|
-
'X-RelayPlane-Latency': latencyMs.toString(),
|
|
1049
|
+
return {
|
|
1050
|
+
success: false,
|
|
1051
|
+
error: {
|
|
1052
|
+
code: `provider_${response.status}`,
|
|
1053
|
+
message: `Provider returned ${response.status}`,
|
|
1054
|
+
status: response.status,
|
|
1055
|
+
retryable: response.status >= 500 || response.status === 429,
|
|
1056
|
+
raw: parsedError,
|
|
1057
|
+
},
|
|
530
1058
|
};
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
});
|
|
544
|
-
proxyReq.on('error', error => {
|
|
545
|
-
stats.requestsHandled++;
|
|
546
|
-
stats.requestsFailed++;
|
|
547
|
-
logUsage({
|
|
548
|
-
timestamp: new Date().toISOString(),
|
|
549
|
-
model,
|
|
550
|
-
provider,
|
|
551
|
-
inputTokens,
|
|
552
|
-
outputTokens: 0,
|
|
553
|
-
cost: 0,
|
|
554
|
-
latencyMs: Date.now() - startMs,
|
|
1059
|
+
}
|
|
1060
|
+
const data = await response.json();
|
|
1061
|
+
const ttftMs = Date.now() - ttftStart;
|
|
1062
|
+
return {
|
|
1063
|
+
success: true,
|
|
1064
|
+
data,
|
|
1065
|
+
usage: data.usage,
|
|
1066
|
+
ttft_ms: ttftMs,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
catch (err) {
|
|
1070
|
+
return {
|
|
555
1071
|
success: false,
|
|
556
|
-
|
|
557
|
-
|
|
1072
|
+
error: {
|
|
1073
|
+
code: 'network_error',
|
|
1074
|
+
message: err instanceof Error ? err.message : 'Network error',
|
|
1075
|
+
status: 500,
|
|
1076
|
+
retryable: true,
|
|
1077
|
+
raw: err,
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Detect auth type from Authorization header
|
|
1084
|
+
*/
|
|
1085
|
+
detectAuthType(authHeader) {
|
|
1086
|
+
if (!authHeader)
|
|
1087
|
+
return 'api';
|
|
1088
|
+
// Consumer auth typically uses session tokens or OAuth
|
|
1089
|
+
// API auth uses API keys starting with specific prefixes
|
|
1090
|
+
if (authHeader.includes('sk-ant-') ||
|
|
1091
|
+
authHeader.includes('sk-') ||
|
|
1092
|
+
authHeader.includes('Bearer sk-')) {
|
|
1093
|
+
return 'api';
|
|
1094
|
+
}
|
|
1095
|
+
// Default to consumer if it looks like a session token
|
|
1096
|
+
if (authHeader.startsWith('Bearer ') && authHeader.length > 100) {
|
|
1097
|
+
return 'consumer';
|
|
1098
|
+
}
|
|
1099
|
+
return 'api';
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Estimate cost based on provider and usage
|
|
1103
|
+
*/
|
|
1104
|
+
estimateCost(provider, usage) {
|
|
1105
|
+
if (!usage)
|
|
1106
|
+
return 0;
|
|
1107
|
+
// Approximate pricing per 1K tokens
|
|
1108
|
+
const pricing = {
|
|
1109
|
+
anthropic: { input: 0.003, output: 0.015 }, // Claude 3.5 Sonnet
|
|
1110
|
+
openai: { input: 0.005, output: 0.015 }, // GPT-4o
|
|
1111
|
+
openrouter: { input: 0.003, output: 0.015 }, // Varies
|
|
1112
|
+
};
|
|
1113
|
+
const rates = pricing[provider] ?? { input: 0.003, output: 0.015 };
|
|
1114
|
+
return ((usage.prompt_tokens / 1000) * rates.input + (usage.completion_tokens / 1000) * rates.output);
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Read request body
|
|
1118
|
+
*/
|
|
1119
|
+
readBody(req) {
|
|
1120
|
+
return new Promise((resolve, reject) => {
|
|
1121
|
+
let body = '';
|
|
1122
|
+
req.on('data', (chunk) => (body += chunk));
|
|
1123
|
+
req.on('end', () => resolve(body));
|
|
1124
|
+
req.on('error', reject);
|
|
558
1125
|
});
|
|
559
|
-
proxyReq.write(JSON.stringify(data));
|
|
560
|
-
proxyReq.end();
|
|
561
1126
|
}
|
|
562
|
-
|
|
563
|
-
|
|
1127
|
+
/**
|
|
1128
|
+
* Send error response
|
|
1129
|
+
*/
|
|
1130
|
+
sendError(res, status, code, message, runId, guidanceUrl) {
|
|
1131
|
+
const error = {
|
|
1132
|
+
error: {
|
|
1133
|
+
message,
|
|
1134
|
+
type: 'relayplane_error',
|
|
1135
|
+
code,
|
|
1136
|
+
run_id: runId,
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1139
|
+
if (guidanceUrl) {
|
|
1140
|
+
error.error['guidance_url'] = guidanceUrl;
|
|
1141
|
+
}
|
|
1142
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1143
|
+
res.end(JSON.stringify(error));
|
|
1144
|
+
}
|
|
1145
|
+
// ============================================================================
|
|
1146
|
+
// Learning Engine Handlers (Phase 5)
|
|
1147
|
+
// ============================================================================
|
|
1148
|
+
async handleAnalyticsSummary(req, res) {
|
|
1149
|
+
try {
|
|
1150
|
+
const workspaceId = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
1151
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
1152
|
+
const from = url.searchParams.get('from') ?? new Date(Date.now() - 7 * 86400000).toISOString();
|
|
1153
|
+
const to = url.searchParams.get('to') ?? new Date().toISOString();
|
|
1154
|
+
const summary = await this.learningEngine.getAnalyticsSummary({
|
|
1155
|
+
workspace_id: workspaceId,
|
|
1156
|
+
from,
|
|
1157
|
+
to,
|
|
1158
|
+
});
|
|
1159
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1160
|
+
res.end(JSON.stringify({ summary }));
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async handleRunAnalysis(req, res) {
|
|
1167
|
+
try {
|
|
1168
|
+
const workspaceId = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
1169
|
+
const result = await this.learningEngine.runAnalysis(workspaceId);
|
|
1170
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1171
|
+
res.end(JSON.stringify(result));
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async handleListSuggestions(req, res) {
|
|
1178
|
+
try {
|
|
1179
|
+
const workspaceId = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
1180
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
1181
|
+
const status = url.searchParams.get('status');
|
|
1182
|
+
const suggestions = await this.learningEngine.listSuggestions({
|
|
1183
|
+
workspace_id: workspaceId,
|
|
1184
|
+
status: status || undefined,
|
|
1185
|
+
});
|
|
1186
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1187
|
+
res.end(JSON.stringify({ suggestions }));
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async handleApproveSuggestion(req, res, id) {
|
|
1194
|
+
try {
|
|
1195
|
+
const body = await this.readBody(req);
|
|
1196
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
1197
|
+
const rule = await this.learningEngine.approveSuggestion(id, {
|
|
1198
|
+
user_id: parsed.user_id ?? 'system',
|
|
1199
|
+
});
|
|
1200
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1201
|
+
res.end(JSON.stringify({ rule }));
|
|
1202
|
+
}
|
|
1203
|
+
catch (err) {
|
|
1204
|
+
this.sendError(res, 400, 'invalid_request', err instanceof Error ? err.message : 'Unknown error');
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async handleRejectSuggestion(req, res, id) {
|
|
1208
|
+
try {
|
|
1209
|
+
const body = await this.readBody(req);
|
|
1210
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
1211
|
+
await this.learningEngine.rejectSuggestion(id, {
|
|
1212
|
+
user_id: parsed.user_id ?? 'system',
|
|
1213
|
+
reason: parsed.reason ?? 'Rejected via API',
|
|
1214
|
+
});
|
|
1215
|
+
res.writeHead(204);
|
|
1216
|
+
res.end();
|
|
1217
|
+
}
|
|
1218
|
+
catch (err) {
|
|
1219
|
+
this.sendError(res, 400, 'invalid_request', err instanceof Error ? err.message : 'Unknown error');
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async handleListRules(req, res) {
|
|
1223
|
+
try {
|
|
1224
|
+
const workspaceId = req.headers['x-relayplane-workspace'] ?? this.config.defaultWorkspaceId;
|
|
1225
|
+
const rules = await this.learningEngine.listRules(workspaceId);
|
|
1226
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1227
|
+
res.end(JSON.stringify({ rules }));
|
|
1228
|
+
}
|
|
1229
|
+
catch (err) {
|
|
1230
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
async handleRuleEffectiveness(res, ruleId) {
|
|
1234
|
+
try {
|
|
1235
|
+
const effectiveness = await this.learningEngine.getRuleEffectiveness(ruleId);
|
|
1236
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1237
|
+
res.end(JSON.stringify({ effectiveness }));
|
|
1238
|
+
}
|
|
1239
|
+
catch (err) {
|
|
1240
|
+
this.sendError(res, 500, 'internal_error', err instanceof Error ? err.message : 'Unknown error');
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Log message
|
|
1245
|
+
*/
|
|
1246
|
+
log(level, message) {
|
|
1247
|
+
if (this.config.verbose || level === 'error') {
|
|
1248
|
+
const timestamp = new Date().toISOString();
|
|
1249
|
+
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Get the ledger instance (useful for testing)
|
|
1254
|
+
*/
|
|
1255
|
+
getLedger() {
|
|
1256
|
+
return this.ledger;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Get the auth gate instance (useful for testing)
|
|
1260
|
+
*/
|
|
1261
|
+
getAuthGate() {
|
|
1262
|
+
return this.authGate;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Get the policy engine instance (useful for testing and policy management)
|
|
1266
|
+
*/
|
|
1267
|
+
getPolicyEngine() {
|
|
1268
|
+
return this.policyEngine;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Get the routing engine instance (Phase 3)
|
|
1272
|
+
*/
|
|
1273
|
+
getRoutingEngine() {
|
|
1274
|
+
return this.routingEngine;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Get the capability registry instance (Phase 3)
|
|
1278
|
+
*/
|
|
1279
|
+
getCapabilityRegistry() {
|
|
1280
|
+
return this.capabilityRegistry;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Get the provider manager instance (Phase 3)
|
|
1284
|
+
*/
|
|
1285
|
+
getProviderManager() {
|
|
1286
|
+
return this.providerManager;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Get the explanation engine instance (Phase 4)
|
|
1290
|
+
*/
|
|
1291
|
+
getExplainer() {
|
|
1292
|
+
return this.explainer;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Get the run comparator instance (Phase 4)
|
|
1296
|
+
*/
|
|
1297
|
+
getComparator() {
|
|
1298
|
+
return this.comparator;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Get the simulator instance (Phase 4)
|
|
1302
|
+
*/
|
|
1303
|
+
getSimulator() {
|
|
1304
|
+
return this.simulator;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Get the learning engine instance (Phase 5)
|
|
1308
|
+
*/
|
|
1309
|
+
getLearningEngine() {
|
|
1310
|
+
return this.learningEngine;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Get the mesh handle instance (Phase 6)
|
|
1314
|
+
*/
|
|
1315
|
+
getMeshHandle() {
|
|
1316
|
+
return this.meshHandle;
|
|
564
1317
|
}
|
|
565
1318
|
}
|
|
1319
|
+
exports.ProxyServer = ProxyServer;
|
|
566
1320
|
/**
|
|
567
|
-
*
|
|
1321
|
+
* Create a new proxy server
|
|
568
1322
|
*/
|
|
569
|
-
function
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
// Health check endpoint
|
|
583
|
-
if (pathname === '/health' && req.method === 'GET') {
|
|
584
|
-
handleHealth(res);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
// Models endpoint
|
|
588
|
-
if (pathname === '/v1/models' && req.method === 'GET') {
|
|
589
|
-
handleModels(res);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
// Chat completions and other API endpoints
|
|
593
|
-
if (pathname.startsWith('/v1/') && req.method === 'POST') {
|
|
594
|
-
handleProxy(req, res, pathname);
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
// Root endpoint
|
|
598
|
-
if (pathname === '/' && req.method === 'GET') {
|
|
599
|
-
sendJson(res, 200, {
|
|
600
|
-
name: 'RelayPlane Proxy',
|
|
601
|
-
version: VERSION,
|
|
602
|
-
status: 'ok',
|
|
603
|
-
endpoints: {
|
|
604
|
-
health: '/health',
|
|
605
|
-
models: '/v1/models',
|
|
606
|
-
chat: '/v1/chat/completions',
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
return;
|
|
1323
|
+
function createProxyServer(config) {
|
|
1324
|
+
return new ProxyServer(config);
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Create a proxy server with optional circuit breaker middleware wrapping.
|
|
1328
|
+
* This gives the advanced proxy the same circuit breaker protection as standalone.
|
|
1329
|
+
*/
|
|
1330
|
+
function createSandboxedProxyServer(config) {
|
|
1331
|
+
const server = createProxyServer(config);
|
|
1332
|
+
if (config?.relayplane?.enabled) {
|
|
1333
|
+
const middleware = new middleware_js_1.RelayPlaneMiddleware((0, relay_config_js_1.resolveConfig)(config.relayplane));
|
|
1334
|
+
return { server, middleware };
|
|
610
1335
|
}
|
|
611
|
-
|
|
612
|
-
sendJson(res, 404, { error: { message: 'Not found' } });
|
|
1336
|
+
return { server };
|
|
613
1337
|
}
|
|
614
|
-
// Create and start server
|
|
615
|
-
const server = http.createServer(handleRequest);
|
|
616
|
-
server.listen(PORT, HOST, () => {
|
|
617
|
-
console.log(`🚀 RelayPlane Proxy v${VERSION}`);
|
|
618
|
-
console.log(` Listening on http://${HOST}:${PORT}`);
|
|
619
|
-
console.log(` Health check: http://${HOST}:${PORT}/health`);
|
|
620
|
-
console.log(` Press Ctrl+C to stop`);
|
|
621
|
-
});
|
|
622
|
-
// Graceful shutdown
|
|
623
|
-
process.on('SIGTERM', () => {
|
|
624
|
-
console.log('\nShutting down...');
|
|
625
|
-
server.close(() => {
|
|
626
|
-
console.log('Server stopped.');
|
|
627
|
-
process.exit(0);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
process.on('SIGINT', () => {
|
|
631
|
-
console.log('\nShutting down...');
|
|
632
|
-
server.close(() => {
|
|
633
|
-
console.log('Server stopped.');
|
|
634
|
-
process.exit(0);
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
1338
|
//# sourceMappingURL=server.js.map
|