@relayplane/proxy 0.2.0 → 1.1.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/README.md +221 -120
- package/dist/server.d.ts +8 -204
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +562 -1014
- package/dist/server.js.map +1 -1
- package/package.json +30 -29
- package/__tests__/server.test.ts +0 -512
- package/__tests__/telemetry.test.ts +0 -126
- package/dist/cli.d.ts +0 -36
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -304
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts +0 -80
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -208
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -74
- package/dist/index.js.map +0 -1
- package/dist/streaming.d.ts +0 -80
- package/dist/streaming.d.ts.map +0 -1
- package/dist/streaming.js +0 -271
- package/dist/streaming.js.map +0 -1
- package/dist/telemetry.d.ts +0 -111
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -315
- package/dist/telemetry.js.map +0 -1
- package/src/cli.ts +0 -341
- package/src/config.ts +0 -206
- package/src/index.ts +0 -82
- package/src/server.ts +0 -1328
- package/src/streaming.ts +0 -331
- package/src/telemetry.ts +0 -343
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -21
package/__tests__/server.test.ts
DELETED
|
@@ -1,512 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proxy Server Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import * as http from 'http';
|
|
7
|
-
import * as fs from 'fs';
|
|
8
|
-
import * as os from 'os';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import { ProxyServer, createProxyServer } from '../src/server.js';
|
|
11
|
-
import { createLedger } from '@relayplane/ledger';
|
|
12
|
-
import { MemoryAuthProfileStorage } from '@relayplane/auth-gate';
|
|
13
|
-
|
|
14
|
-
// Helper to make HTTP requests
|
|
15
|
-
async function request(
|
|
16
|
-
port: number,
|
|
17
|
-
method: string,
|
|
18
|
-
path: string,
|
|
19
|
-
body?: unknown,
|
|
20
|
-
headers?: Record<string, string>
|
|
21
|
-
): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: unknown }> {
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
const req = http.request(
|
|
24
|
-
{
|
|
25
|
-
hostname: '127.0.0.1',
|
|
26
|
-
port,
|
|
27
|
-
path,
|
|
28
|
-
method,
|
|
29
|
-
headers: {
|
|
30
|
-
'Content-Type': 'application/json',
|
|
31
|
-
...headers,
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
(res) => {
|
|
35
|
-
let data = '';
|
|
36
|
-
res.on('data', (chunk) => (data += chunk));
|
|
37
|
-
res.on('end', () => {
|
|
38
|
-
let parsed: unknown;
|
|
39
|
-
try {
|
|
40
|
-
parsed = JSON.parse(data);
|
|
41
|
-
} catch {
|
|
42
|
-
parsed = data;
|
|
43
|
-
}
|
|
44
|
-
resolve({
|
|
45
|
-
status: res.statusCode ?? 500,
|
|
46
|
-
headers: res.headers,
|
|
47
|
-
body: parsed,
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
req.on('error', reject);
|
|
54
|
-
|
|
55
|
-
if (body) {
|
|
56
|
-
req.write(JSON.stringify(body));
|
|
57
|
-
}
|
|
58
|
-
req.end();
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
describe('ProxyServer', () => {
|
|
63
|
-
let server: ProxyServer;
|
|
64
|
-
let dbPath: string;
|
|
65
|
-
const port = 3099; // Use non-standard port for testing
|
|
66
|
-
|
|
67
|
-
beforeEach(async () => {
|
|
68
|
-
// Use temp directory for ledger
|
|
69
|
-
dbPath = path.join(os.tmpdir(), `proxy-test-${Date.now()}.db`);
|
|
70
|
-
|
|
71
|
-
const ledger = createLedger({ dbPath });
|
|
72
|
-
const authStorage = new MemoryAuthProfileStorage();
|
|
73
|
-
|
|
74
|
-
// Seed test auth profiles
|
|
75
|
-
await authStorage.seedTestData('test_workspace');
|
|
76
|
-
|
|
77
|
-
server = createProxyServer({
|
|
78
|
-
port,
|
|
79
|
-
ledger,
|
|
80
|
-
authStorage,
|
|
81
|
-
verbose: false,
|
|
82
|
-
defaultWorkspaceId: 'test_workspace',
|
|
83
|
-
defaultAgentId: 'test_agent',
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await server.start();
|
|
87
|
-
// Wait a bit for server to be fully ready
|
|
88
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
afterEach(async () => {
|
|
92
|
-
await server.stop();
|
|
93
|
-
await server.getLedger().close();
|
|
94
|
-
|
|
95
|
-
// Clean up test database
|
|
96
|
-
if (fs.existsSync(dbPath)) {
|
|
97
|
-
fs.unlinkSync(dbPath);
|
|
98
|
-
}
|
|
99
|
-
if (fs.existsSync(dbPath + '-wal')) {
|
|
100
|
-
fs.unlinkSync(dbPath + '-wal');
|
|
101
|
-
}
|
|
102
|
-
if (fs.existsSync(dbPath + '-shm')) {
|
|
103
|
-
fs.unlinkSync(dbPath + '-shm');
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('Health endpoint', () => {
|
|
108
|
-
it('should respond to /health', async () => {
|
|
109
|
-
const res = await request(port, 'GET', '/health');
|
|
110
|
-
|
|
111
|
-
expect(res.status).toBe(200);
|
|
112
|
-
expect(res.body).toEqual({ status: 'ok', version: '0.1.0' });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should respond to /', async () => {
|
|
116
|
-
const res = await request(port, 'GET', '/');
|
|
117
|
-
|
|
118
|
-
expect(res.status).toBe(200);
|
|
119
|
-
expect(res.body).toEqual({ status: 'ok', version: '0.1.0' });
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('Models endpoint', () => {
|
|
124
|
-
it('should list available models', async () => {
|
|
125
|
-
const res = await request(port, 'GET', '/v1/models');
|
|
126
|
-
|
|
127
|
-
expect(res.status).toBe(200);
|
|
128
|
-
const body = res.body as { object: string; data: Array<{ id: string }> };
|
|
129
|
-
expect(body.object).toBe('list');
|
|
130
|
-
expect(body.data.length).toBeGreaterThan(0);
|
|
131
|
-
expect(body.data.some((m) => m.id === 'claude-3-5-sonnet')).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('Chat completions endpoint', () => {
|
|
136
|
-
it('should return error when provider not configured', async () => {
|
|
137
|
-
const res = await request(
|
|
138
|
-
port,
|
|
139
|
-
'POST',
|
|
140
|
-
'/v1/chat/completions',
|
|
141
|
-
{
|
|
142
|
-
model: 'claude-3-5-sonnet',
|
|
143
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
'X-RelayPlane-Workspace': 'test_workspace',
|
|
147
|
-
'X-RelayPlane-Agent': 'test_agent',
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
expect(res.status).toBe(500);
|
|
152
|
-
const body = res.body as { error: { code: string; run_id?: string } };
|
|
153
|
-
expect(body.error.code).toBe('provider_not_configured');
|
|
154
|
-
// Should still have a run_id for debugging
|
|
155
|
-
expect(body.error.run_id).toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should record run in ledger even on failure', async () => {
|
|
159
|
-
await request(
|
|
160
|
-
port,
|
|
161
|
-
'POST',
|
|
162
|
-
'/v1/chat/completions',
|
|
163
|
-
{
|
|
164
|
-
model: 'gpt-4o',
|
|
165
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
'X-RelayPlane-Workspace': 'test_workspace',
|
|
169
|
-
'X-RelayPlane-Agent': 'test_agent',
|
|
170
|
-
}
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
// Check ledger has the run
|
|
174
|
-
const runs = await server.getLedger().queryRuns({
|
|
175
|
-
workspace_id: 'test_workspace',
|
|
176
|
-
limit: 10,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
expect(runs.items.length).toBeGreaterThan(0);
|
|
180
|
-
expect(runs.items[0]?.status).toBe('failed');
|
|
181
|
-
expect(runs.items[0]?.agent_id).toBe('test_agent');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should track auth_type from header detection', async () => {
|
|
185
|
-
// Make request with API key style auth
|
|
186
|
-
await request(
|
|
187
|
-
port,
|
|
188
|
-
'POST',
|
|
189
|
-
'/v1/chat/completions',
|
|
190
|
-
{
|
|
191
|
-
model: 'claude-3-5-sonnet',
|
|
192
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
'Authorization': 'Bearer sk-ant-api123',
|
|
196
|
-
'X-RelayPlane-Workspace': 'test_workspace',
|
|
197
|
-
}
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const runs = await server.getLedger().queryRuns({
|
|
201
|
-
workspace_id: 'test_workspace',
|
|
202
|
-
limit: 1,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
expect(runs.items[0]?.auth_type).toBe('api');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('should detect automated requests via header', async () => {
|
|
209
|
-
await request(
|
|
210
|
-
port,
|
|
211
|
-
'POST',
|
|
212
|
-
'/v1/chat/completions',
|
|
213
|
-
{
|
|
214
|
-
model: 'claude-3-5-sonnet',
|
|
215
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
'X-RelayPlane-Automated': 'true',
|
|
219
|
-
'X-RelayPlane-Workspace': 'test_workspace',
|
|
220
|
-
}
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
const runs = await server.getLedger().queryRuns({
|
|
224
|
-
workspace_id: 'test_workspace',
|
|
225
|
-
limit: 1,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
expect(runs.items[0]?.execution_mode).toBe('background');
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
describe('CORS', () => {
|
|
233
|
-
it('should handle OPTIONS preflight', async () => {
|
|
234
|
-
const res = await request(port, 'OPTIONS', '/v1/chat/completions');
|
|
235
|
-
|
|
236
|
-
expect(res.status).toBe(204);
|
|
237
|
-
expect(res.headers['access-control-allow-origin']).toBe('*');
|
|
238
|
-
expect(res.headers['access-control-allow-methods']).toContain('POST');
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe('404 handling', () => {
|
|
243
|
-
it('should return 404 for unknown routes', async () => {
|
|
244
|
-
const res = await request(port, 'GET', '/v1/unknown');
|
|
245
|
-
|
|
246
|
-
expect(res.status).toBe(404);
|
|
247
|
-
const body = res.body as { error: { code: string } };
|
|
248
|
-
expect(body.error.code).toBe('not_found');
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
describe('createProxyServer', () => {
|
|
254
|
-
it('should create server with default config', () => {
|
|
255
|
-
const server = createProxyServer();
|
|
256
|
-
expect(server).toBeInstanceOf(ProxyServer);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('should create server with custom config', () => {
|
|
260
|
-
const server = createProxyServer({
|
|
261
|
-
port: 9999,
|
|
262
|
-
host: '0.0.0.0',
|
|
263
|
-
verbose: true,
|
|
264
|
-
});
|
|
265
|
-
expect(server).toBeInstanceOf(ProxyServer);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
describe('Policy Integration', () => {
|
|
270
|
-
let server: ProxyServer;
|
|
271
|
-
let dbPath: string;
|
|
272
|
-
const port = 3098;
|
|
273
|
-
|
|
274
|
-
beforeEach(async () => {
|
|
275
|
-
dbPath = path.join(os.tmpdir(), `proxy-policy-test-${Date.now()}.db`);
|
|
276
|
-
|
|
277
|
-
const ledger = createLedger({ dbPath });
|
|
278
|
-
const authStorage = new MemoryAuthProfileStorage();
|
|
279
|
-
|
|
280
|
-
await authStorage.seedTestData('test_workspace');
|
|
281
|
-
|
|
282
|
-
server = createProxyServer({
|
|
283
|
-
port,
|
|
284
|
-
ledger,
|
|
285
|
-
authStorage,
|
|
286
|
-
verbose: false,
|
|
287
|
-
defaultWorkspaceId: 'test_workspace',
|
|
288
|
-
defaultAgentId: 'test_agent',
|
|
289
|
-
enforcePolicies: true,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await server.start();
|
|
293
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
afterEach(async () => {
|
|
297
|
-
await server.stop();
|
|
298
|
-
await server.getLedger().close();
|
|
299
|
-
|
|
300
|
-
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
301
|
-
if (fs.existsSync(dbPath + '-wal')) fs.unlinkSync(dbPath + '-wal');
|
|
302
|
-
if (fs.existsSync(dbPath + '-shm')) fs.unlinkSync(dbPath + '-shm');
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
describe('Policy Management API', () => {
|
|
306
|
-
it('should create and list policies', async () => {
|
|
307
|
-
// Create a policy via API
|
|
308
|
-
const createRes = await request(
|
|
309
|
-
port,
|
|
310
|
-
'POST',
|
|
311
|
-
'/v1/policies',
|
|
312
|
-
{
|
|
313
|
-
workspace_id: 'test_workspace',
|
|
314
|
-
name: 'No Opus',
|
|
315
|
-
description: 'Block expensive model',
|
|
316
|
-
type: 'model.denylist',
|
|
317
|
-
enabled: true,
|
|
318
|
-
priority: 100,
|
|
319
|
-
scope: { applies_to: 'workspace' },
|
|
320
|
-
conditions: [],
|
|
321
|
-
action: { type: 'deny', parameters: { models: ['claude-3-opus'] } },
|
|
322
|
-
created_by: 'test_user',
|
|
323
|
-
},
|
|
324
|
-
{ 'X-RelayPlane-Workspace': 'test_workspace' }
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
expect(createRes.status).toBe(201);
|
|
328
|
-
const created = createRes.body as { policy: { policy_id: string; name: string } };
|
|
329
|
-
expect(created.policy.name).toBe('No Opus');
|
|
330
|
-
expect(created.policy.policy_id).toBeDefined();
|
|
331
|
-
|
|
332
|
-
// List policies
|
|
333
|
-
const listRes = await request(port, 'GET', '/v1/policies', undefined, {
|
|
334
|
-
'X-RelayPlane-Workspace': 'test_workspace',
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
expect(listRes.status).toBe(200);
|
|
338
|
-
const list = listRes.body as { policies: Array<{ name: string }> };
|
|
339
|
-
expect(list.policies.some((p) => p.name === 'No Opus')).toBe(true);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('should get policy by ID', async () => {
|
|
343
|
-
const createRes = await request(
|
|
344
|
-
port,
|
|
345
|
-
'POST',
|
|
346
|
-
'/v1/policies',
|
|
347
|
-
{
|
|
348
|
-
workspace_id: 'test_workspace',
|
|
349
|
-
name: 'Test Policy',
|
|
350
|
-
description: '',
|
|
351
|
-
type: 'model.allowlist',
|
|
352
|
-
enabled: true,
|
|
353
|
-
priority: 100,
|
|
354
|
-
scope: { applies_to: 'workspace' },
|
|
355
|
-
conditions: [],
|
|
356
|
-
action: { type: 'deny' },
|
|
357
|
-
created_by: 'test_user',
|
|
358
|
-
},
|
|
359
|
-
{ 'X-RelayPlane-Workspace': 'test_workspace' }
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
const policyId = (createRes.body as { policy: { policy_id: string } }).policy.policy_id;
|
|
363
|
-
|
|
364
|
-
const getRes = await request(port, 'GET', `/v1/policies/${policyId}`);
|
|
365
|
-
|
|
366
|
-
expect(getRes.status).toBe(200);
|
|
367
|
-
const got = getRes.body as { policy: { policy_id: string } };
|
|
368
|
-
expect(got.policy.policy_id).toBe(policyId);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('should update policy', async () => {
|
|
372
|
-
const createRes = await request(
|
|
373
|
-
port,
|
|
374
|
-
'POST',
|
|
375
|
-
'/v1/policies',
|
|
376
|
-
{
|
|
377
|
-
workspace_id: 'test_workspace',
|
|
378
|
-
name: 'Original',
|
|
379
|
-
description: '',
|
|
380
|
-
type: 'model.allowlist',
|
|
381
|
-
enabled: true,
|
|
382
|
-
priority: 100,
|
|
383
|
-
scope: { applies_to: 'workspace' },
|
|
384
|
-
conditions: [],
|
|
385
|
-
action: { type: 'deny' },
|
|
386
|
-
created_by: 'test_user',
|
|
387
|
-
},
|
|
388
|
-
{ 'X-RelayPlane-Workspace': 'test_workspace' }
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
const policyId = (createRes.body as { policy: { policy_id: string } }).policy.policy_id;
|
|
392
|
-
|
|
393
|
-
const updateRes = await request(port, 'PATCH', `/v1/policies/${policyId}`, {
|
|
394
|
-
name: 'Updated',
|
|
395
|
-
priority: 50,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
expect(updateRes.status).toBe(200);
|
|
399
|
-
const updated = updateRes.body as { policy: { name: string; priority: number } };
|
|
400
|
-
expect(updated.policy.name).toBe('Updated');
|
|
401
|
-
expect(updated.policy.priority).toBe(50);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('should delete policy', async () => {
|
|
405
|
-
const createRes = await request(
|
|
406
|
-
port,
|
|
407
|
-
'POST',
|
|
408
|
-
'/v1/policies',
|
|
409
|
-
{
|
|
410
|
-
workspace_id: 'test_workspace',
|
|
411
|
-
name: 'To Delete',
|
|
412
|
-
description: '',
|
|
413
|
-
type: 'model.allowlist',
|
|
414
|
-
enabled: true,
|
|
415
|
-
priority: 100,
|
|
416
|
-
scope: { applies_to: 'workspace' },
|
|
417
|
-
conditions: [],
|
|
418
|
-
action: { type: 'deny' },
|
|
419
|
-
created_by: 'test_user',
|
|
420
|
-
},
|
|
421
|
-
{ 'X-RelayPlane-Workspace': 'test_workspace' }
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
const policyId = (createRes.body as { policy: { policy_id: string } }).policy.policy_id;
|
|
425
|
-
|
|
426
|
-
const deleteRes = await request(port, 'DELETE', `/v1/policies/${policyId}`);
|
|
427
|
-
expect(deleteRes.status).toBe(204);
|
|
428
|
-
|
|
429
|
-
const getRes = await request(port, 'GET', `/v1/policies/${policyId}`);
|
|
430
|
-
expect(getRes.status).toBe(404);
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
describe('Policy Testing (Dry Run)', () => {
|
|
435
|
-
it('should test policy without side effects', async () => {
|
|
436
|
-
// Create a blocking policy
|
|
437
|
-
await request(
|
|
438
|
-
port,
|
|
439
|
-
'POST',
|
|
440
|
-
'/v1/policies',
|
|
441
|
-
{
|
|
442
|
-
workspace_id: 'test_workspace',
|
|
443
|
-
name: 'Block Opus',
|
|
444
|
-
description: '',
|
|
445
|
-
type: 'model.denylist',
|
|
446
|
-
enabled: true,
|
|
447
|
-
priority: 100,
|
|
448
|
-
scope: { applies_to: 'workspace' },
|
|
449
|
-
conditions: [],
|
|
450
|
-
action: { type: 'deny', parameters: { models: ['claude-3-opus'] } },
|
|
451
|
-
created_by: 'test_user',
|
|
452
|
-
},
|
|
453
|
-
{ 'X-RelayPlane-Workspace': 'test_workspace' }
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
// Test the policy
|
|
457
|
-
const testRes = await request(port, 'POST', '/v1/policies/test', {
|
|
458
|
-
workspace_id: 'test_workspace',
|
|
459
|
-
agent_id: 'test_agent',
|
|
460
|
-
request: {
|
|
461
|
-
model: 'claude-3-opus',
|
|
462
|
-
provider: 'anthropic',
|
|
463
|
-
},
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
expect(testRes.status).toBe(200);
|
|
467
|
-
const result = testRes.body as { decision: { allow: boolean; action: string } };
|
|
468
|
-
expect(result.decision.allow).toBe(false);
|
|
469
|
-
expect(result.decision.action).toBe('deny');
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
describe('Policy Enforcement', () => {
|
|
474
|
-
it('should deny request when policy blocks model', async () => {
|
|
475
|
-
// Create a blocking policy
|
|
476
|
-
await request(
|
|
477
|
-
port,
|
|
478
|
-
'POST',
|
|
479
|
-
'/v1/policies',
|
|
480
|
-
{
|
|
481
|
-
workspace_id: 'test_workspace',
|
|
482
|
-
name: 'Block GPT',
|
|
483
|
-
description: '',
|
|
484
|
-
type: 'model.denylist',
|
|
485
|
-
enabled: true,
|
|
486
|
-
priority: 100,
|
|
487
|
-
scope: { applies_to: 'workspace' },
|
|
488
|
-
conditions: [],
|
|
489
|
-
action: { type: 'deny', parameters: { models: ['gpt-4o'] } },
|
|
490
|
-
created_by: 'test_user',
|
|
491
|
-
},
|
|
492
|
-
{ 'X-RelayPlane-Workspace': 'test_workspace' }
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
// Try to use blocked model
|
|
496
|
-
const res = await request(
|
|
497
|
-
port,
|
|
498
|
-
'POST',
|
|
499
|
-
'/v1/chat/completions',
|
|
500
|
-
{
|
|
501
|
-
model: 'gpt-4o',
|
|
502
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
503
|
-
},
|
|
504
|
-
{ 'X-RelayPlane-Workspace': 'test_workspace' }
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
expect(res.status).toBe(403);
|
|
508
|
-
const body = res.body as { error: { code: string } };
|
|
509
|
-
expect(body.error.code).toBe('policy_denied');
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Telemetry Module
|
|
3
|
-
*
|
|
4
|
-
* Tests: collect → audit → opt-out
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
-
import * as fs from 'fs';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import * as os from 'os';
|
|
11
|
-
|
|
12
|
-
describe('Telemetry Module Tests', () => {
|
|
13
|
-
describe('Task Type Inference', () => {
|
|
14
|
-
it('should infer task types correctly', async () => {
|
|
15
|
-
// Import the module dynamically
|
|
16
|
-
const { inferTaskType } = await import('../src/telemetry.js');
|
|
17
|
-
|
|
18
|
-
// Quick task (small input/output)
|
|
19
|
-
expect(inferTaskType(100, 50, 'gpt-4')).toBe('quick_task');
|
|
20
|
-
|
|
21
|
-
// Long context (> 10000 input)
|
|
22
|
-
expect(inferTaskType(15000, 500, 'claude-3')).toBe('long_context');
|
|
23
|
-
|
|
24
|
-
// Generation (high output ratio)
|
|
25
|
-
expect(inferTaskType(100, 600, 'gpt-4')).toBe('generation');
|
|
26
|
-
|
|
27
|
-
// Classification (low output ratio)
|
|
28
|
-
expect(inferTaskType(500, 50, 'gpt-4')).toBe('classification');
|
|
29
|
-
|
|
30
|
-
// Code review
|
|
31
|
-
expect(inferTaskType(3000, 800, 'claude-3')).toBe('code_review');
|
|
32
|
-
|
|
33
|
-
// Content generation
|
|
34
|
-
expect(inferTaskType(500, 1500, 'gpt-4')).toBe('content_generation');
|
|
35
|
-
|
|
36
|
-
// Tool use
|
|
37
|
-
expect(inferTaskType(500, 200, 'gpt-4', true)).toBe('tool_use');
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('Cost Estimation', () => {
|
|
42
|
-
it('should estimate costs correctly', async () => {
|
|
43
|
-
const { estimateCost } = await import('../src/telemetry.js');
|
|
44
|
-
|
|
45
|
-
// Claude 3.5 Haiku pricing: $0.8/M in, $4/M out
|
|
46
|
-
const haikuCost = estimateCost('claude-3-5-haiku-20241022', 1000, 1000);
|
|
47
|
-
expect(haikuCost).toBeCloseTo(0.0048, 4);
|
|
48
|
-
|
|
49
|
-
// GPT-4o pricing: $2.5/M in, $10/M out
|
|
50
|
-
const gpt4oCost = estimateCost('gpt-4o', 1000, 1000);
|
|
51
|
-
expect(gpt4oCost).toBeCloseTo(0.0125, 4);
|
|
52
|
-
|
|
53
|
-
// Unknown model uses default pricing
|
|
54
|
-
const unknownCost = estimateCost('unknown-model', 1000, 1000);
|
|
55
|
-
expect(unknownCost).toBeGreaterThan(0);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('Audit Mode', () => {
|
|
60
|
-
it('should buffer events in audit mode', async () => {
|
|
61
|
-
const {
|
|
62
|
-
setAuditMode,
|
|
63
|
-
isAuditMode,
|
|
64
|
-
getAuditBuffer,
|
|
65
|
-
clearAuditBuffer,
|
|
66
|
-
} = await import('../src/telemetry.js');
|
|
67
|
-
|
|
68
|
-
// Clear any previous buffer
|
|
69
|
-
clearAuditBuffer();
|
|
70
|
-
|
|
71
|
-
// Check initial state
|
|
72
|
-
expect(isAuditMode()).toBe(false);
|
|
73
|
-
|
|
74
|
-
// Enable audit mode
|
|
75
|
-
setAuditMode(true);
|
|
76
|
-
expect(isAuditMode()).toBe(true);
|
|
77
|
-
|
|
78
|
-
// Check buffer is empty
|
|
79
|
-
expect(getAuditBuffer()).toHaveLength(0);
|
|
80
|
-
|
|
81
|
-
// Clean up
|
|
82
|
-
setAuditMode(false);
|
|
83
|
-
clearAuditBuffer();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('Offline Mode', () => {
|
|
88
|
-
it('should track offline mode setting', async () => {
|
|
89
|
-
const {
|
|
90
|
-
setOfflineMode,
|
|
91
|
-
isOfflineMode
|
|
92
|
-
} = await import('../src/telemetry.js');
|
|
93
|
-
|
|
94
|
-
// Initially false
|
|
95
|
-
expect(isOfflineMode()).toBe(false);
|
|
96
|
-
|
|
97
|
-
// Enable offline mode
|
|
98
|
-
setOfflineMode(true);
|
|
99
|
-
expect(isOfflineMode()).toBe(true);
|
|
100
|
-
|
|
101
|
-
// Disable offline mode
|
|
102
|
-
setOfflineMode(false);
|
|
103
|
-
expect(isOfflineMode()).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('Telemetry Stats', () => {
|
|
108
|
-
it('should return stats structure', async () => {
|
|
109
|
-
const { getTelemetryStats } = await import('../src/telemetry.js');
|
|
110
|
-
|
|
111
|
-
const stats = getTelemetryStats();
|
|
112
|
-
|
|
113
|
-
// Verify structure
|
|
114
|
-
expect(stats).toHaveProperty('totalEvents');
|
|
115
|
-
expect(stats).toHaveProperty('totalCost');
|
|
116
|
-
expect(stats).toHaveProperty('byModel');
|
|
117
|
-
expect(stats).toHaveProperty('byTaskType');
|
|
118
|
-
expect(stats).toHaveProperty('successRate');
|
|
119
|
-
|
|
120
|
-
// All should be valid types
|
|
121
|
-
expect(typeof stats.totalEvents).toBe('number');
|
|
122
|
-
expect(typeof stats.totalCost).toBe('number');
|
|
123
|
-
expect(typeof stats.successRate).toBe('number');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
});
|
package/dist/cli.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* RelayPlane Proxy CLI
|
|
4
|
-
*
|
|
5
|
-
* Intelligent AI model routing proxy server.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* npx @relayplane/proxy [command] [options]
|
|
9
|
-
* relayplane-proxy [command] [options]
|
|
10
|
-
*
|
|
11
|
-
* Commands:
|
|
12
|
-
* (default) Start the proxy server
|
|
13
|
-
* telemetry [on|off|status] Manage telemetry settings
|
|
14
|
-
* stats Show usage statistics
|
|
15
|
-
* config Show configuration
|
|
16
|
-
*
|
|
17
|
-
* Options:
|
|
18
|
-
* --port <number> Port to listen on (default: 3001)
|
|
19
|
-
* --host <string> Host to bind to (default: 127.0.0.1)
|
|
20
|
-
* --offline Disable all network calls except LLM endpoints
|
|
21
|
-
* --audit Show telemetry payloads before sending
|
|
22
|
-
* -v, --verbose Enable verbose logging
|
|
23
|
-
* -h, --help Show this help message
|
|
24
|
-
* --version Show version
|
|
25
|
-
*
|
|
26
|
-
* Environment Variables:
|
|
27
|
-
* ANTHROPIC_API_KEY Anthropic API key
|
|
28
|
-
* OPENAI_API_KEY OpenAI API key
|
|
29
|
-
* GEMINI_API_KEY Google Gemini API key
|
|
30
|
-
* XAI_API_KEY xAI/Grok API key
|
|
31
|
-
* MOONSHOT_API_KEY Moonshot API key
|
|
32
|
-
*
|
|
33
|
-
* @packageDocumentation
|
|
34
|
-
*/
|
|
35
|
-
export {};
|
|
36
|
-
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG"}
|