rlhf-feedback-loop 0.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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/adapters/README.md +8 -0
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +20 -0
- package/adapters/chatgpt/INSTALL.md +80 -0
- package/adapters/chatgpt/openapi.yaml +292 -0
- package/adapters/claude/.mcp.json +8 -0
- package/adapters/codex/config.toml +4 -0
- package/adapters/gemini/function-declarations.json +95 -0
- package/adapters/mcp/server-stdio.js +444 -0
- package/bin/cli.js +167 -0
- package/config/mcp-allowlists.json +29 -0
- package/config/policy-bundles/constrained-v1.json +53 -0
- package/config/policy-bundles/default-v1.json +80 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/subagent-profiles.json +32 -0
- package/openapi/openapi.yaml +292 -0
- package/package.json +91 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +31 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +57 -0
- package/plugins/gemini-extension/INSTALL.md +74 -0
- package/plugins/gemini-extension/gemini_prompt.txt +10 -0
- package/plugins/gemini-extension/tool_contract.json +28 -0
- package/scripts/billing.js +471 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/code-reasoning.js +307 -0
- package/scripts/context-engine.js +547 -0
- package/scripts/contextfs.js +513 -0
- package/scripts/contract-audit.js +198 -0
- package/scripts/dpo-optimizer.js +208 -0
- package/scripts/export-dpo-pairs.js +316 -0
- package/scripts/export-training.js +448 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +838 -0
- package/scripts/feedback-schema.js +300 -0
- package/scripts/feedback-to-memory.js +165 -0
- package/scripts/feedback-to-rules.js +109 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/intent-router.js +164 -0
- package/scripts/mcp-policy.js +92 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/prove-adapters.js +364 -0
- package/scripts/prove-attribution.js +364 -0
- package/scripts/prove-automation.js +393 -0
- package/scripts/prove-data-quality.js +219 -0
- package/scripts/prove-intelligence.js +256 -0
- package/scripts/prove-lancedb.js +370 -0
- package/scripts/prove-loop-closure.js +255 -0
- package/scripts/prove-rlaif.js +404 -0
- package/scripts/prove-subway-upgrades.js +250 -0
- package/scripts/prove-training-export.js +324 -0
- package/scripts/prove-v2-milestone.js +273 -0
- package/scripts/prove-v3-milestone.js +381 -0
- package/scripts/rlaif-self-audit.js +123 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/self-heal.js +127 -0
- package/scripts/self-healing-check.js +111 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +29 -0
- package/scripts/thompson-sampling.js +331 -0
- package/scripts/train_from_feedback.py +914 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/vector-store.js +100 -0
- package/src/api/server.js +497 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const pkg = require('../../package.json');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
captureFeedback,
|
|
9
|
+
analyzeFeedback,
|
|
10
|
+
feedbackSummary,
|
|
11
|
+
writePreventionRules,
|
|
12
|
+
getFeedbackPaths,
|
|
13
|
+
} = require('../../scripts/feedback-loop');
|
|
14
|
+
const {
|
|
15
|
+
readJSONL,
|
|
16
|
+
exportDpoFromMemories,
|
|
17
|
+
DEFAULT_LOCAL_MEMORY_LOG,
|
|
18
|
+
} = require('../../scripts/export-dpo-pairs');
|
|
19
|
+
const {
|
|
20
|
+
ensureContextFs,
|
|
21
|
+
normalizeNamespaces,
|
|
22
|
+
constructContextPack,
|
|
23
|
+
evaluateContextPack,
|
|
24
|
+
getProvenance,
|
|
25
|
+
} = require('../../scripts/contextfs');
|
|
26
|
+
const {
|
|
27
|
+
buildRubricEvaluation,
|
|
28
|
+
} = require('../../scripts/rubric-engine');
|
|
29
|
+
const {
|
|
30
|
+
listIntents,
|
|
31
|
+
planIntent,
|
|
32
|
+
} = require('../../scripts/intent-router');
|
|
33
|
+
const {
|
|
34
|
+
createCheckoutSession,
|
|
35
|
+
provisionApiKey,
|
|
36
|
+
validateApiKey,
|
|
37
|
+
recordUsage,
|
|
38
|
+
handleWebhook,
|
|
39
|
+
verifyWebhookSignature,
|
|
40
|
+
loadKeyStore,
|
|
41
|
+
} = require('../../scripts/billing');
|
|
42
|
+
|
|
43
|
+
function getSafeDataDir() {
|
|
44
|
+
const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
|
|
45
|
+
return path.resolve(path.dirname(FEEDBACK_LOG_PATH));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createHttpError(statusCode, message) {
|
|
49
|
+
const err = new Error(message);
|
|
50
|
+
err.statusCode = statusCode;
|
|
51
|
+
return err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sendJson(res, statusCode, payload) {
|
|
55
|
+
const body = JSON.stringify(payload);
|
|
56
|
+
res.writeHead(statusCode, {
|
|
57
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
58
|
+
'Content-Length': Buffer.byteLength(body),
|
|
59
|
+
});
|
|
60
|
+
res.end(body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sendText(res, statusCode, text) {
|
|
64
|
+
res.writeHead(statusCode, {
|
|
65
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
66
|
+
'Content-Length': Buffer.byteLength(text),
|
|
67
|
+
});
|
|
68
|
+
res.end(text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseJsonBody(req, maxBytes = 1024 * 1024) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let total = 0;
|
|
74
|
+
const chunks = [];
|
|
75
|
+
|
|
76
|
+
req.on('data', (chunk) => {
|
|
77
|
+
total += chunk.length;
|
|
78
|
+
if (total > maxBytes) {
|
|
79
|
+
reject(createHttpError(413, 'Request body too large'));
|
|
80
|
+
req.destroy();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
chunks.push(chunk);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
req.on('end', () => {
|
|
87
|
+
if (chunks.length === 0) {
|
|
88
|
+
resolve({});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
|
|
93
|
+
} catch {
|
|
94
|
+
reject(createHttpError(400, 'Invalid JSON body'));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
req.on('error', reject);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseOptionalObject(input, name) {
|
|
103
|
+
if (input == null) return {};
|
|
104
|
+
if (typeof input === 'object' && !Array.isArray(input)) return input;
|
|
105
|
+
if (typeof input === 'string') {
|
|
106
|
+
const trimmed = input.trim();
|
|
107
|
+
if (!trimmed) return {};
|
|
108
|
+
const parsed = JSON.parse(trimmed);
|
|
109
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
110
|
+
throw createHttpError(400, `${name} must be an object`);
|
|
111
|
+
}
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
throw createHttpError(400, `${name} must be an object`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getExpectedApiKey() {
|
|
118
|
+
if (process.env.RLHF_ALLOW_INSECURE === 'true') return null;
|
|
119
|
+
const configured = process.env.RLHF_API_KEY;
|
|
120
|
+
if (!configured) {
|
|
121
|
+
throw new Error('RLHF_API_KEY is required unless RLHF_ALLOW_INSECURE=true');
|
|
122
|
+
}
|
|
123
|
+
return configured;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isAuthorized(req, expected) {
|
|
127
|
+
if (!expected) return true;
|
|
128
|
+
const auth = req.headers.authorization || '';
|
|
129
|
+
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
130
|
+
|
|
131
|
+
// Check static RLHF_API_KEY first
|
|
132
|
+
if (token === expected) return true;
|
|
133
|
+
|
|
134
|
+
// Also accept any valid provisioned billing key
|
|
135
|
+
if (token) {
|
|
136
|
+
const result = validateApiKey(token);
|
|
137
|
+
return result.valid === true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract the Bearer token from a request (returns '' if absent).
|
|
145
|
+
*/
|
|
146
|
+
function extractBearerToken(req) {
|
|
147
|
+
const auth = req.headers.authorization || '';
|
|
148
|
+
return auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractTags(input) {
|
|
152
|
+
if (Array.isArray(input)) return input;
|
|
153
|
+
if (typeof input === 'string') {
|
|
154
|
+
return input
|
|
155
|
+
.split(',')
|
|
156
|
+
.map((t) => t.trim())
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
}
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveSafePath(inputPath, { mustExist = false } = {}) {
|
|
163
|
+
const allowExternal = process.env.RLHF_ALLOW_EXTERNAL_PATHS === 'true';
|
|
164
|
+
const resolved = path.resolve(String(inputPath || ''));
|
|
165
|
+
const SAFE_DATA_DIR = getSafeDataDir();
|
|
166
|
+
const inSafeRoot = resolved === SAFE_DATA_DIR || resolved.startsWith(`${SAFE_DATA_DIR}${path.sep}`);
|
|
167
|
+
|
|
168
|
+
if (!allowExternal && !inSafeRoot) {
|
|
169
|
+
throw createHttpError(400, `Path must stay within ${SAFE_DATA_DIR}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (mustExist && !fs.existsSync(resolved)) {
|
|
173
|
+
throw createHttpError(400, `Path does not exist: ${resolved}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return resolved;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createApiServer() {
|
|
180
|
+
const expectedApiKey = getExpectedApiKey();
|
|
181
|
+
|
|
182
|
+
return http.createServer(async (req, res) => {
|
|
183
|
+
const parsed = new URL(req.url, 'http://localhost');
|
|
184
|
+
const pathname = parsed.pathname;
|
|
185
|
+
|
|
186
|
+
// Health check is unauthenticated — required for Railway/load-balancer probes
|
|
187
|
+
if (req.method === 'GET' && pathname === '/health') {
|
|
188
|
+
sendJson(res, 200, {
|
|
189
|
+
status: 'ok',
|
|
190
|
+
version: pkg.version,
|
|
191
|
+
uptime: process.uptime(),
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Stripe webhook is unauthenticated — uses HMAC signature verification instead
|
|
197
|
+
if (req.method === 'POST' && pathname === '/v1/billing/webhook') {
|
|
198
|
+
try {
|
|
199
|
+
const rawBody = await new Promise((resolve, reject) => {
|
|
200
|
+
const chunks = [];
|
|
201
|
+
req.on('data', (c) => chunks.push(c));
|
|
202
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
203
|
+
req.on('error', reject);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const sig = req.headers['stripe-signature'] || '';
|
|
207
|
+
if (!verifyWebhookSignature(rawBody, sig)) {
|
|
208
|
+
sendJson(res, 400, { error: 'Invalid webhook signature' });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let event;
|
|
213
|
+
try {
|
|
214
|
+
event = JSON.parse(rawBody.toString('utf-8'));
|
|
215
|
+
} catch {
|
|
216
|
+
sendJson(res, 400, { error: 'Invalid JSON in webhook body' });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = handleWebhook(event);
|
|
221
|
+
sendJson(res, 200, result);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err.statusCode) {
|
|
224
|
+
sendJson(res, err.statusCode, { error: err.message });
|
|
225
|
+
} else {
|
|
226
|
+
sendJson(res, 500, { error: err.message || 'Internal Server Error' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!isAuthorized(req, expectedApiKey)) {
|
|
233
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Usage metering — record request for billing keys (not static RLHF_API_KEY)
|
|
238
|
+
const _token = extractBearerToken(req);
|
|
239
|
+
if (_token && _token !== expectedApiKey) {
|
|
240
|
+
recordUsage(_token);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
if (req.method === 'GET' && pathname === '/healthz') {
|
|
245
|
+
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = getFeedbackPaths();
|
|
246
|
+
sendJson(res, 200, {
|
|
247
|
+
status: 'ok',
|
|
248
|
+
feedbackLogPath: FEEDBACK_LOG_PATH,
|
|
249
|
+
memoryLogPath: MEMORY_LOG_PATH,
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
255
|
+
sendJson(res, 200, analyzeFeedback());
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (req.method === 'GET' && pathname === '/v1/intents/catalog') {
|
|
260
|
+
const mcpProfile = parsed.searchParams.get('mcpProfile') || undefined;
|
|
261
|
+
const bundleId = parsed.searchParams.get('bundleId') || undefined;
|
|
262
|
+
try {
|
|
263
|
+
const catalog = listIntents({ mcpProfile, bundleId });
|
|
264
|
+
sendJson(res, 200, catalog);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
throw createHttpError(400, err.message || 'Invalid intent catalog request');
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (req.method === 'POST' && pathname === '/v1/intents/plan') {
|
|
272
|
+
const body = await parseJsonBody(req);
|
|
273
|
+
try {
|
|
274
|
+
const plan = planIntent({
|
|
275
|
+
intentId: body.intentId,
|
|
276
|
+
context: body.context || '',
|
|
277
|
+
mcpProfile: body.mcpProfile,
|
|
278
|
+
bundleId: body.bundleId,
|
|
279
|
+
approved: body.approved === true,
|
|
280
|
+
});
|
|
281
|
+
sendJson(res, 200, plan);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
throw createHttpError(400, err.message || 'Invalid intent plan request');
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (req.method === 'GET' && pathname === '/v1/feedback/summary') {
|
|
289
|
+
const recent = Number(parsed.searchParams.get('recent') || 20);
|
|
290
|
+
const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20);
|
|
291
|
+
sendJson(res, 200, { summary });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (req.method === 'POST' && pathname === '/v1/feedback/capture') {
|
|
296
|
+
const body = await parseJsonBody(req);
|
|
297
|
+
const result = captureFeedback({
|
|
298
|
+
signal: body.signal,
|
|
299
|
+
context: body.context || '',
|
|
300
|
+
whatWentWrong: body.whatWentWrong,
|
|
301
|
+
whatToChange: body.whatToChange,
|
|
302
|
+
whatWorked: body.whatWorked,
|
|
303
|
+
rubricScores: body.rubricScores,
|
|
304
|
+
guardrails: body.guardrails,
|
|
305
|
+
tags: extractTags(body.tags),
|
|
306
|
+
skill: body.skill,
|
|
307
|
+
});
|
|
308
|
+
const code = result.accepted ? 200 : 422;
|
|
309
|
+
sendJson(res, code, result);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (req.method === 'POST' && pathname === '/v1/feedback/rules') {
|
|
314
|
+
const body = await parseJsonBody(req);
|
|
315
|
+
const minOccurrences = Number(body.minOccurrences || 2);
|
|
316
|
+
const outputPath = body.outputPath ? resolveSafePath(body.outputPath) : undefined;
|
|
317
|
+
const result = writePreventionRules(outputPath, Number.isFinite(minOccurrences) ? minOccurrences : 2);
|
|
318
|
+
sendJson(res, 200, {
|
|
319
|
+
path: result.path,
|
|
320
|
+
markdown: result.markdown,
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (req.method === 'POST' && pathname === '/v1/dpo/export') {
|
|
326
|
+
const body = await parseJsonBody(req);
|
|
327
|
+
let memories = [];
|
|
328
|
+
|
|
329
|
+
if (body.inputPath) {
|
|
330
|
+
const safeInputPath = resolveSafePath(body.inputPath, { mustExist: true });
|
|
331
|
+
const raw = fs.readFileSync(safeInputPath, 'utf-8');
|
|
332
|
+
const parsedMemories = JSON.parse(raw);
|
|
333
|
+
memories = Array.isArray(parsedMemories) ? parsedMemories : parsedMemories.memories || [];
|
|
334
|
+
} else {
|
|
335
|
+
const localPath = body.memoryLogPath
|
|
336
|
+
? resolveSafePath(body.memoryLogPath, { mustExist: true })
|
|
337
|
+
: DEFAULT_LOCAL_MEMORY_LOG;
|
|
338
|
+
memories = readJSONL(localPath);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const result = exportDpoFromMemories(memories);
|
|
342
|
+
if (body.outputPath) {
|
|
343
|
+
const safeOutputPath = resolveSafePath(body.outputPath);
|
|
344
|
+
fs.mkdirSync(path.dirname(safeOutputPath), { recursive: true });
|
|
345
|
+
fs.writeFileSync(safeOutputPath, result.jsonl);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
sendJson(res, 200, {
|
|
349
|
+
pairs: result.pairs.length,
|
|
350
|
+
errors: result.errors.length,
|
|
351
|
+
learnings: result.learnings.length,
|
|
352
|
+
unpairedErrors: result.unpairedErrors.length,
|
|
353
|
+
unpairedLearnings: result.unpairedLearnings.length,
|
|
354
|
+
outputPath: body.outputPath ? resolveSafePath(body.outputPath) : null,
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (req.method === 'POST' && pathname === '/v1/context/construct') {
|
|
360
|
+
const body = await parseJsonBody(req);
|
|
361
|
+
ensureContextFs();
|
|
362
|
+
let namespaces = [];
|
|
363
|
+
try {
|
|
364
|
+
namespaces = normalizeNamespaces(Array.isArray(body.namespaces) ? body.namespaces : []);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
throw createHttpError(400, err.message || 'Invalid namespaces');
|
|
367
|
+
}
|
|
368
|
+
const pack = constructContextPack({
|
|
369
|
+
query: body.query || '',
|
|
370
|
+
maxItems: Number(body.maxItems || 8),
|
|
371
|
+
maxChars: Number(body.maxChars || 6000),
|
|
372
|
+
namespaces,
|
|
373
|
+
});
|
|
374
|
+
sendJson(res, 200, pack);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (req.method === 'POST' && pathname === '/v1/context/evaluate') {
|
|
379
|
+
const body = await parseJsonBody(req);
|
|
380
|
+
if (!body.packId || !body.outcome) {
|
|
381
|
+
throw createHttpError(400, 'packId and outcome are required');
|
|
382
|
+
}
|
|
383
|
+
let rubricEvaluation = null;
|
|
384
|
+
if (body.rubricScores != null || body.guardrails != null) {
|
|
385
|
+
try {
|
|
386
|
+
rubricEvaluation = buildRubricEvaluation({
|
|
387
|
+
rubricScores: body.rubricScores,
|
|
388
|
+
guardrails: parseOptionalObject(body.guardrails, 'guardrails'),
|
|
389
|
+
});
|
|
390
|
+
} catch (err) {
|
|
391
|
+
throw createHttpError(400, `Invalid rubric payload: ${err.message}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const evaluation = evaluateContextPack({
|
|
395
|
+
packId: body.packId,
|
|
396
|
+
outcome: body.outcome,
|
|
397
|
+
signal: body.signal || null,
|
|
398
|
+
notes: body.notes || '',
|
|
399
|
+
rubricEvaluation,
|
|
400
|
+
});
|
|
401
|
+
sendJson(res, 200, evaluation);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (req.method === 'GET' && pathname === '/v1/context/provenance') {
|
|
406
|
+
const limit = Number(parsed.searchParams.get('limit') || 50);
|
|
407
|
+
const events = getProvenance(Number.isFinite(limit) ? limit : 50);
|
|
408
|
+
sendJson(res, 200, { events });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (req.method === 'GET' && pathname === '/') {
|
|
413
|
+
sendText(res, 200, 'RLHF Feedback Loop API is running.');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ----------------------------------------------------------------
|
|
418
|
+
// Billing routes
|
|
419
|
+
// ----------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
// POST /v1/billing/checkout — create Stripe Checkout session
|
|
422
|
+
if (req.method === 'POST' && pathname === '/v1/billing/checkout') {
|
|
423
|
+
const body = await parseJsonBody(req);
|
|
424
|
+
const result = await createCheckoutSession({
|
|
425
|
+
successUrl: body.successUrl,
|
|
426
|
+
cancelUrl: body.cancelUrl,
|
|
427
|
+
customerEmail: body.customerEmail,
|
|
428
|
+
});
|
|
429
|
+
sendJson(res, 200, result);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// GET /v1/billing/usage — usage for the authenticated key
|
|
434
|
+
if (req.method === 'GET' && pathname === '/v1/billing/usage') {
|
|
435
|
+
const token = extractBearerToken(req);
|
|
436
|
+
const validation = validateApiKey(token);
|
|
437
|
+
if (!validation.valid) {
|
|
438
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
sendJson(res, 200, {
|
|
442
|
+
key: token,
|
|
443
|
+
customerId: validation.customerId,
|
|
444
|
+
usageCount: validation.usageCount,
|
|
445
|
+
});
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// POST /v1/billing/provision — manually provision key (admin)
|
|
450
|
+
if (req.method === 'POST' && pathname === '/v1/billing/provision') {
|
|
451
|
+
const body = await parseJsonBody(req);
|
|
452
|
+
if (!body.customerId) {
|
|
453
|
+
throw createHttpError(400, 'customerId is required');
|
|
454
|
+
}
|
|
455
|
+
const result = provisionApiKey(body.customerId);
|
|
456
|
+
sendJson(res, 200, result);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
sendJson(res, 404, { error: 'Not Found' });
|
|
461
|
+
} catch (err) {
|
|
462
|
+
if (err.statusCode) {
|
|
463
|
+
sendJson(res, err.statusCode, { error: err.message });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
sendJson(res, 500, { error: err.message || 'Internal Server Error' });
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function startServer({ port } = {}) {
|
|
472
|
+
const listenPort = Number(port ?? process.env.PORT ?? 8787);
|
|
473
|
+
const server = createApiServer();
|
|
474
|
+
return new Promise((resolve) => {
|
|
475
|
+
server.listen(listenPort, () => {
|
|
476
|
+
const address = server.address();
|
|
477
|
+
const actualPort = (address && typeof address === 'object' && address.port)
|
|
478
|
+
? address.port
|
|
479
|
+
: listenPort;
|
|
480
|
+
resolve({
|
|
481
|
+
server,
|
|
482
|
+
port: actualPort,
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = {
|
|
489
|
+
createApiServer,
|
|
490
|
+
startServer,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
if (require.main === module) {
|
|
494
|
+
startServer().then(({ port }) => {
|
|
495
|
+
console.log(`RLHF API listening on http://localhost:${port}`);
|
|
496
|
+
});
|
|
497
|
+
}
|