rightmodeler 0.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.
@@ -0,0 +1,841 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ appendFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ truncateSync,
8
+ } from "node:fs";
9
+ import { createServer, request as httpRequest } from "node:http";
10
+ import { request as httpsRequest } from "node:https";
11
+ import { join } from "node:path";
12
+ import { PassThrough } from "node:stream";
13
+
14
+ import { hopByHopHeaders } from "./headers.js";
15
+ import { classifyStream } from "../transport/stream.js";
16
+
17
+ const maxRequestBytes = 10 * 1024 * 1024;
18
+ const egressSourceHeader = "x-rightmodeler-egress-source";
19
+ const streamIdleTimeoutMs = Number(
20
+ process.env.RM_STREAM_IDLE_TIMEOUT_MS ?? 30_000,
21
+ );
22
+ const streamHardDeadlineMs = Number(
23
+ process.env.RM_STREAM_HARD_DEADLINE_MS ?? 300_000,
24
+ );
25
+
26
+ function requiredEnv(name) {
27
+ const value = process.env[name];
28
+ if (value === undefined || value.length === 0) {
29
+ throw new Error(`${name} is required`);
30
+ }
31
+ return value;
32
+ }
33
+
34
+ function jsonEnv(name) {
35
+ return JSON.parse(requiredEnv(name));
36
+ }
37
+
38
+ function isObject(value) {
39
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40
+ }
41
+
42
+ function parseConfig() {
43
+ const rawSwapPolicy = jsonEnv("RM_SWAP_POLICY");
44
+ if (
45
+ !isObject(rawSwapPolicy) ||
46
+ Object.values(rawSwapPolicy).some(
47
+ (model) => typeof model !== "string" || model.length === 0,
48
+ )
49
+ ) {
50
+ throw new Error("RM_SWAP_POLICY must map step ids to model ids");
51
+ }
52
+ const swapPolicy = Object.assign(Object.create(null), rawSwapPolicy);
53
+
54
+ const rawPricing = jsonEnv("RM_PRICING_TABLE");
55
+ if (!isObject(rawPricing)) {
56
+ throw new Error("RM_PRICING_TABLE must be an object");
57
+ }
58
+ const pricingTable = {};
59
+ for (const [model, pricing] of Object.entries(rawPricing)) {
60
+ if (
61
+ model.length === 0 ||
62
+ !isObject(pricing) ||
63
+ !Number.isFinite(pricing.input) ||
64
+ pricing.input < 0 ||
65
+ !Number.isFinite(pricing.output) ||
66
+ pricing.output < 0
67
+ ) {
68
+ throw new Error(
69
+ "RM_PRICING_TABLE values must contain non-negative input and output prices",
70
+ );
71
+ }
72
+ pricingTable[model] = { input: pricing.input, output: pricing.output };
73
+ }
74
+
75
+ const lease = jsonEnv("RM_BUDGET_LEASE");
76
+ if (!isObject(lease) || !Number.isFinite(lease.maxUsd) || lease.maxUsd < 0) {
77
+ throw new Error("RM_BUDGET_LEASE must contain a non-negative maxUsd");
78
+ }
79
+
80
+ const portText = requiredEnv("RM_PROXY_PORT");
81
+ const port = Number(portText);
82
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
83
+ throw new Error("RM_PROXY_PORT must be an integer from 0 through 65535");
84
+ }
85
+
86
+ const egressUrl = new URL(requiredEnv("RM_EGRESS_URL"));
87
+ if (!/^https?:$/.test(egressUrl.protocol)) {
88
+ throw new Error("RM_EGRESS_URL must use http or https");
89
+ }
90
+
91
+ return {
92
+ runId: requiredEnv("RM_RUN_ID"),
93
+ caseId: requiredEnv("RM_CASE_ID"),
94
+ executionId: requiredEnv("RM_EXECUTION_ID"),
95
+ scratch: requiredEnv("RM_SCRATCH"),
96
+ host: process.env.RM_PROXY_HOST ?? "0.0.0.0",
97
+ port,
98
+ egressUrl,
99
+ swapPolicy,
100
+ pricingTable,
101
+ lease,
102
+ };
103
+ }
104
+
105
+ function readJsonLines(path, label) {
106
+ if (!existsSync(path)) return [];
107
+ const source = readFileSync(path, "utf8");
108
+ if (source.length === 0) return [];
109
+ const lines = source.split("\n");
110
+ lines.pop();
111
+ if (!source.endsWith("\n")) {
112
+ truncateSync(path, source.lastIndexOf("\n") + 1);
113
+ }
114
+ return lines.map((line, index) => {
115
+ try {
116
+ return JSON.parse(line);
117
+ } catch {
118
+ throw new Error(`${label} has malformed JSON on line ${index + 1}`);
119
+ }
120
+ });
121
+ }
122
+
123
+ function loadState(spoolPath, checkpointPath) {
124
+ const state = {
125
+ seqPos: 0,
126
+ lastAttemptGroup: 0,
127
+ spentUsd: 0,
128
+ groups: new Map(),
129
+ attemptIds: new Set(),
130
+ reservations: new Map(),
131
+ };
132
+
133
+ for (const row of readJsonLines(checkpointPath, "checkpoint spool")) {
134
+ if (
135
+ !isObject(row) ||
136
+ !Number.isSafeInteger(row.seqPos) ||
137
+ row.seqPos <= state.seqPos ||
138
+ !Number.isSafeInteger(row.attemptGroup) ||
139
+ row.attemptGroup < 1
140
+ ) {
141
+ throw new Error("checkpoint spool contains an invalid row");
142
+ }
143
+ state.seqPos = row.seqPos;
144
+ state.lastAttemptGroup = Math.max(state.lastAttemptGroup, row.attemptGroup);
145
+ if (typeof row.logicalCallId === "string") {
146
+ state.groups.set(row.logicalCallId, row.attemptGroup);
147
+ }
148
+ }
149
+
150
+ for (const row of readJsonLines(spoolPath, "attempt spool")) {
151
+ if (!isObject(row)) continue;
152
+ if (row.kind === "attempt_reservation") {
153
+ if (
154
+ typeof row.attemptId !== "string" ||
155
+ typeof row.logicalCallId !== "string" ||
156
+ !Number.isSafeInteger(row.attemptGroup) ||
157
+ row.attemptGroup < 1 ||
158
+ !Number.isFinite(row.reservedUsd) ||
159
+ row.reservedUsd < 0
160
+ ) {
161
+ throw new Error("attempt spool contains an invalid reservation");
162
+ }
163
+ state.reservations.set(row.attemptId, row);
164
+ state.lastAttemptGroup = Math.max(
165
+ state.lastAttemptGroup,
166
+ row.attemptGroup,
167
+ );
168
+ state.groups.set(row.logicalCallId, row.attemptGroup);
169
+ state.attemptIds.add(row.attemptId);
170
+ continue;
171
+ }
172
+ if (row.kind !== "request_attempt") continue;
173
+ if (
174
+ !Number.isSafeInteger(row.attemptGroup) ||
175
+ row.attemptGroup < 1 ||
176
+ typeof row.logicalCallId !== "string" ||
177
+ typeof row.attemptId !== "string" ||
178
+ !Number.isFinite(row.costUsd) ||
179
+ row.costUsd < 0
180
+ ) {
181
+ throw new Error("attempt spool contains an invalid request attempt");
182
+ }
183
+ state.reservations.delete(row.attemptId);
184
+ state.lastAttemptGroup = Math.max(state.lastAttemptGroup, row.attemptGroup);
185
+ state.groups.set(row.logicalCallId, row.attemptGroup);
186
+ state.attemptIds.add(row.attemptId);
187
+ state.spentUsd += row.costUsd;
188
+ }
189
+
190
+ for (const reservation of state.reservations.values()) {
191
+ state.spentUsd += reservation.reservedUsd;
192
+ }
193
+
194
+ return state;
195
+ }
196
+
197
+ function appendRow(path, row) {
198
+ appendFileSync(path, `${JSON.stringify(row)}\n`, "utf8");
199
+ }
200
+
201
+ function header(request, name) {
202
+ const value = request.headers[name];
203
+ return typeof value === "string" && value.length > 0 ? value : null;
204
+ }
205
+
206
+ function hasDuplicateHeader(request, name) {
207
+ return (request.headersDistinct[name]?.length ?? 0) > 1;
208
+ }
209
+
210
+ function responseHeaders(headers) {
211
+ const forwarded = {};
212
+ for (const [name, value] of Object.entries(headers)) {
213
+ if (
214
+ value !== undefined &&
215
+ !hopByHopHeaders.has(name) &&
216
+ name !== egressSourceHeader &&
217
+ name !== "content-length"
218
+ ) {
219
+ forwarded[name] = value;
220
+ }
221
+ }
222
+ return forwarded;
223
+ }
224
+
225
+ function requestHeaders(headers, bodyLength) {
226
+ const forwarded = {};
227
+ for (const [name, value] of Object.entries(headers)) {
228
+ if (
229
+ value !== undefined &&
230
+ !hopByHopHeaders.has(name) &&
231
+ name !== "authorization" &&
232
+ name !== "host" &&
233
+ name !== "content-length"
234
+ ) {
235
+ forwarded[name] = value;
236
+ }
237
+ }
238
+ forwarded["content-length"] = String(bodyLength);
239
+ return forwarded;
240
+ }
241
+
242
+ function sendJson(response, status, body) {
243
+ const bytes = Buffer.from(JSON.stringify(body));
244
+ response.writeHead(status, {
245
+ "content-type": "application/json",
246
+ "content-length": bytes.length,
247
+ });
248
+ response.end(bytes);
249
+ }
250
+
251
+ function readCappedBody(request) {
252
+ return new Promise((resolve, reject) => {
253
+ let chunks = [];
254
+ let bytes = 0;
255
+ let settled = false;
256
+ request.on("data", (chunk) => {
257
+ bytes += chunk.length;
258
+ if (settled) return;
259
+ if (bytes > maxRequestBytes) {
260
+ settled = true;
261
+ chunks = [];
262
+ resolve({ tooLarge: true, bytes });
263
+ return;
264
+ }
265
+ chunks.push(Buffer.from(chunk));
266
+ });
267
+ request.once("end", () => {
268
+ if (!settled) {
269
+ settled = true;
270
+ resolve({ tooLarge: false, body: Buffer.concat(chunks), bytes });
271
+ }
272
+ });
273
+ request.once("error", (error) => {
274
+ if (!settled) reject(error);
275
+ });
276
+ });
277
+ }
278
+
279
+ function requestUpstream(
280
+ url,
281
+ requestTarget,
282
+ method,
283
+ headers,
284
+ body,
285
+ deadlineMs,
286
+ ) {
287
+ const send = url.protocol === "https:" ? httpsRequest : httpRequest;
288
+ return new Promise((resolve, reject) => {
289
+ const request = send(
290
+ url,
291
+ { method, path: requestTarget, headers },
292
+ (response) => {
293
+ clearTimeout(deadline);
294
+ resolve(response);
295
+ },
296
+ );
297
+ const deadline = setTimeout(() => {
298
+ request.destroy(new Error("Upstream response header deadline exceeded"));
299
+ }, deadlineMs);
300
+ request.once("error", reject);
301
+ request.once("close", () => clearTimeout(deadline));
302
+ request.end(body);
303
+ });
304
+ }
305
+
306
+ function writeWithBackpressure(stream, chunk) {
307
+ if (stream.destroyed || stream.writableEnded) return Promise.resolve(false);
308
+ if (stream.write(chunk)) return Promise.resolve(true);
309
+ return new Promise((resolve) => {
310
+ const cleanup = () => {
311
+ stream.off("drain", onDrain);
312
+ stream.off("close", onClose);
313
+ stream.off("error", onClose);
314
+ };
315
+ const onDrain = () => {
316
+ cleanup();
317
+ resolve(true);
318
+ };
319
+ const onClose = () => {
320
+ cleanup();
321
+ resolve(false);
322
+ };
323
+ stream.once("drain", onDrain);
324
+ stream.once("close", onClose);
325
+ stream.once("error", onClose);
326
+ if (stream.destroyed || stream.writableEnded) onClose();
327
+ });
328
+ }
329
+
330
+ function normalizeUsage(usage, maxInputTokens, maxOutputTokens) {
331
+ if (!isObject(usage)) return null;
332
+ const inputTokens =
333
+ usage.prompt_tokens ?? usage.input_tokens ?? usage.inputTokens;
334
+ const outputTokens =
335
+ usage.completion_tokens ?? usage.output_tokens ?? usage.outputTokens;
336
+ const totalTokens = usage.total_tokens ?? usage.totalTokens;
337
+ if (
338
+ !Number.isSafeInteger(inputTokens) ||
339
+ inputTokens < 0 ||
340
+ inputTokens > maxInputTokens ||
341
+ !Number.isSafeInteger(outputTokens) ||
342
+ outputTokens < 0 ||
343
+ outputTokens > maxOutputTokens
344
+ ) {
345
+ return null;
346
+ }
347
+ return {
348
+ inputTokens,
349
+ outputTokens,
350
+ totalTokens:
351
+ Number.isSafeInteger(totalTokens) && totalTokens >= 0
352
+ ? totalTokens
353
+ : inputTokens + outputTokens,
354
+ };
355
+ }
356
+
357
+ function usageCharge(usage, pricing, reservedUsd) {
358
+ if (usage === null) return reservedUsd;
359
+ return (
360
+ usage.inputTokens * pricing.input + usage.outputTokens * pricing.output
361
+ );
362
+ }
363
+
364
+ async function forwardStreaming(upstream, outgoing, status, spoolSink) {
365
+ outgoing.writeHead(status, responseHeaders(upstream.headers));
366
+ const classifierBytes = new PassThrough();
367
+ classifierBytes.on("error", () => undefined);
368
+ const abortController = new AbortController();
369
+ let clientCancelled = false;
370
+ let classificationDone = false;
371
+ let upstreamFailed = false;
372
+ let proxyTerminated = false;
373
+ outgoing.once("close", () => {
374
+ if (!proxyTerminated && !upstreamFailed && !outgoing.writableEnded) {
375
+ clientCancelled = true;
376
+ abortController.abort();
377
+ upstream.destroy();
378
+ classifierBytes.end();
379
+ }
380
+ });
381
+
382
+ const classification = Promise.resolve(
383
+ classifyStream(classifierBytes, {
384
+ format: "openai-chat-completions",
385
+ idleTimeoutMs: streamIdleTimeoutMs,
386
+ hardDeadlineMs: streamHardDeadlineMs,
387
+ signal: abortController.signal,
388
+ httpStatus: status,
389
+ spoolSink,
390
+ }),
391
+ ).then((result) => {
392
+ classificationDone = true;
393
+ proxyTerminated = true;
394
+ if (!upstream.complete) {
395
+ upstream.destroy();
396
+ }
397
+ if (
398
+ result.outcome === "truncated" &&
399
+ (result.reason === "idle" || result.reason === "deadline")
400
+ ) {
401
+ if (!outgoing.destroyed) outgoing.destroy();
402
+ }
403
+ return result;
404
+ });
405
+
406
+ try {
407
+ for await (const chunk of upstream) {
408
+ if (clientCancelled) break;
409
+ if (!(await writeWithBackpressure(outgoing, chunk))) {
410
+ clientCancelled = true;
411
+ break;
412
+ }
413
+ if (!classificationDone) {
414
+ await writeWithBackpressure(classifierBytes, chunk);
415
+ }
416
+ }
417
+ if (!classifierBytes.destroyed && !classifierBytes.writableEnded) {
418
+ classifierBytes.end();
419
+ }
420
+ } catch {
421
+ if (!proxyTerminated) {
422
+ upstreamFailed = true;
423
+ abortController.abort();
424
+ classifierBytes.end();
425
+ if (!outgoing.destroyed) outgoing.destroy();
426
+ }
427
+ }
428
+
429
+ const result = await classification;
430
+ return {
431
+ streamOutcome: clientCancelled
432
+ ? "client_cancelled"
433
+ : upstreamFailed
434
+ ? "truncated"
435
+ : result.outcome,
436
+ usage: result.usage ?? null,
437
+ spoolPath: result.spoolPath ?? null,
438
+ finishedWithoutSentinel: result.finishedWithoutSentinel === true,
439
+ upstreamFailed,
440
+ };
441
+ }
442
+
443
+ async function forwardNonStreaming(upstream, outgoing, status) {
444
+ outgoing.writeHead(status, responseHeaders(upstream.headers));
445
+ const chunks = [];
446
+ let clientCancelled = false;
447
+ let upstreamFailed = false;
448
+ outgoing.once("close", () => {
449
+ if (!upstreamFailed && !outgoing.writableEnded) {
450
+ clientCancelled = true;
451
+ upstream.destroy();
452
+ }
453
+ });
454
+
455
+ try {
456
+ for await (const chunk of upstream) {
457
+ chunks.push(Buffer.from(chunk));
458
+ if (!(await writeWithBackpressure(outgoing, chunk))) {
459
+ clientCancelled = true;
460
+ upstream.destroy();
461
+ break;
462
+ }
463
+ }
464
+ } catch {
465
+ upstreamFailed = true;
466
+ if (!outgoing.destroyed) outgoing.destroy();
467
+ }
468
+
469
+ if (clientCancelled) {
470
+ return {
471
+ streamOutcome: "client_cancelled",
472
+ usage: null,
473
+ upstreamFailed: false,
474
+ };
475
+ }
476
+ if (upstreamFailed) {
477
+ return { streamOutcome: "truncated", usage: null, upstreamFailed: true };
478
+ }
479
+ if (status >= 400) {
480
+ return {
481
+ streamOutcome: "provider_error",
482
+ usage: null,
483
+ upstreamFailed: false,
484
+ };
485
+ }
486
+ try {
487
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
488
+ return {
489
+ streamOutcome: "completed",
490
+ usage: isObject(body) ? (body.usage ?? null) : null,
491
+ upstreamFailed: false,
492
+ };
493
+ } catch {
494
+ return { streamOutcome: "truncated", usage: null, upstreamFailed: false };
495
+ }
496
+ }
497
+
498
+ function nextAttemptId(state) {
499
+ const attemptId = randomUUID();
500
+ state.attemptIds.add(attemptId);
501
+ return attemptId;
502
+ }
503
+
504
+ async function main() {
505
+ const config = parseConfig();
506
+ const proxyRoot = join(config.scratch, "proxy");
507
+ const attemptDirectory = join(proxyRoot, "attempts");
508
+ const checkpointDirectory = join(proxyRoot, "checkpoints");
509
+ const streamDirectory = join(proxyRoot, "streams");
510
+ mkdirSync(attemptDirectory, { recursive: true });
511
+ mkdirSync(checkpointDirectory, { recursive: true });
512
+ mkdirSync(streamDirectory, { recursive: true });
513
+ const spoolPath = join(attemptDirectory, `${config.executionId}.0.jsonl`);
514
+ const checkpointPath = join(checkpointDirectory, `${config.caseId}.jsonl`);
515
+ const state = loadState(spoolPath, checkpointPath);
516
+ let reservedUsd = 0;
517
+
518
+ function checkpoint(logicalCallId) {
519
+ let attemptGroup =
520
+ logicalCallId === null ? undefined : state.groups.get(logicalCallId);
521
+ if (attemptGroup === undefined) {
522
+ state.lastAttemptGroup += 1;
523
+ attemptGroup = state.lastAttemptGroup;
524
+ if (logicalCallId !== null) {
525
+ state.groups.set(logicalCallId, attemptGroup);
526
+ }
527
+ }
528
+ state.seqPos += 1;
529
+ appendRow(checkpointPath, {
530
+ caseId: config.caseId,
531
+ seqPos: state.seqPos,
532
+ attemptGroup,
533
+ logicalCallId,
534
+ });
535
+ return attemptGroup;
536
+ }
537
+
538
+ function recordLost({
539
+ attemptGroup,
540
+ logicalCallId,
541
+ stepId,
542
+ reason,
543
+ startedAt,
544
+ }) {
545
+ const endedAt = new Date().toISOString();
546
+ appendRow(spoolPath, {
547
+ kind: "lost",
548
+ runId: config.runId,
549
+ caseId: config.caseId,
550
+ stepId,
551
+ executionId: config.executionId,
552
+ attemptId: nextAttemptId(state),
553
+ logicalCallId,
554
+ attemptGroup,
555
+ attribution: "lost",
556
+ streamOutcome: null,
557
+ usage: null,
558
+ costUsd: null,
559
+ costIsEstimate: false,
560
+ reservedUsd: 0,
561
+ leaseChargeUsd: 0,
562
+ rejectionReason: reason,
563
+ startedAt,
564
+ endedAt,
565
+ });
566
+ }
567
+
568
+ async function handle(incoming, outgoing) {
569
+ const startedAt = new Date().toISOString();
570
+ const duplicateStep = hasDuplicateHeader(incoming, "x-rm-step");
571
+ const stepId = duplicateStep ? null : header(incoming, "x-rm-step");
572
+ const logicalCallId = header(incoming, "x-rm-call");
573
+ const attemptGroup = checkpoint(logicalCallId);
574
+
575
+ if (duplicateStep || stepId === null || logicalCallId === null) {
576
+ incoming.resume();
577
+ recordLost({
578
+ attemptGroup,
579
+ logicalCallId,
580
+ stepId,
581
+ reason: duplicateStep
582
+ ? "duplicate_step_correlation"
583
+ : "missing_correlation",
584
+ startedAt,
585
+ });
586
+ if (duplicateStep) {
587
+ sendJson(outgoing, 400, {
588
+ error: "Duplicate correlation header: x-rm-step",
589
+ });
590
+ return;
591
+ }
592
+ const missing = stepId === null ? "x-rm-step" : "x-rm-call";
593
+ sendJson(outgoing, 400, {
594
+ error: `Missing required correlation header: ${missing}`,
595
+ });
596
+ return;
597
+ }
598
+
599
+ const requestBody = await readCappedBody(incoming);
600
+ if (requestBody.tooLarge) {
601
+ recordLost({
602
+ attemptGroup,
603
+ logicalCallId,
604
+ stepId,
605
+ reason: "request_too_large",
606
+ startedAt,
607
+ });
608
+ sendJson(outgoing, 413, { error: "Request body exceeds 10 MiB." });
609
+ return;
610
+ }
611
+
612
+ let parsed;
613
+ try {
614
+ parsed = JSON.parse(requestBody.body.toString("utf8"));
615
+ } catch {
616
+ recordLost({
617
+ attemptGroup,
618
+ logicalCallId,
619
+ stepId,
620
+ reason: "malformed_json",
621
+ startedAt,
622
+ });
623
+ sendJson(outgoing, 400, { error: "Request body must be valid JSON." });
624
+ return;
625
+ }
626
+
627
+ const maxTokens = parsed?.max_tokens;
628
+ if (
629
+ !isObject(parsed) ||
630
+ typeof parsed.model !== "string" ||
631
+ parsed.model.length === 0 ||
632
+ !Number.isSafeInteger(maxTokens) ||
633
+ maxTokens < 0
634
+ ) {
635
+ recordLost({
636
+ attemptGroup,
637
+ logicalCallId,
638
+ stepId,
639
+ reason: "invalid_request",
640
+ startedAt,
641
+ });
642
+ sendJson(outgoing, 400, {
643
+ error: "Request body requires model and a non-negative max_tokens.",
644
+ });
645
+ return;
646
+ }
647
+
648
+ const model = config.swapPolicy[stepId] ?? parsed.model;
649
+ const rewritten = { ...parsed, model };
650
+ const forwardedBody = Buffer.from(JSON.stringify(rewritten));
651
+ const pricing = config.pricingTable[model];
652
+ if (pricing === undefined) {
653
+ recordLost({
654
+ attemptGroup,
655
+ logicalCallId,
656
+ stepId,
657
+ reason: "missing_pricing",
658
+ startedAt,
659
+ });
660
+ sendJson(outgoing, 400, {
661
+ error: `Pricing is unavailable for model: ${model}`,
662
+ });
663
+ return;
664
+ }
665
+
666
+ const estimatedInputTokens = forwardedBody.length;
667
+ const estimatedWorstCaseUsd =
668
+ estimatedInputTokens * pricing.input + maxTokens * pricing.output;
669
+ const requiredLeaseUsd =
670
+ state.spentUsd + reservedUsd + estimatedWorstCaseUsd;
671
+ if (requiredLeaseUsd > config.lease.maxUsd) {
672
+ appendRow(spoolPath, {
673
+ kind: "blocked",
674
+ runId: config.runId,
675
+ caseId: config.caseId,
676
+ stepId,
677
+ executionId: config.executionId,
678
+ logicalCallId,
679
+ attemptGroup,
680
+ reason: "budget",
681
+ model,
682
+ estimatedInputTokens,
683
+ maxOutputTokens: maxTokens,
684
+ estimatedWorstCaseUsd,
685
+ lease: { maxUsd: config.lease.maxUsd },
686
+ requiredLease: { maxUsd: requiredLeaseUsd },
687
+ timestamp: new Date().toISOString(),
688
+ });
689
+ sendJson(outgoing, 402, {
690
+ error: "Budget lease cannot cover the next request.",
691
+ requiredLease: { maxUsd: requiredLeaseUsd },
692
+ });
693
+ return;
694
+ }
695
+
696
+ reservedUsd += estimatedWorstCaseUsd;
697
+ const attemptId = nextAttemptId(state);
698
+ const responseSpoolPath = join(streamDirectory, `${attemptId}.txt`);
699
+ appendRow(spoolPath, {
700
+ kind: "attempt_reservation",
701
+ runId: config.runId,
702
+ caseId: config.caseId,
703
+ stepId,
704
+ executionId: config.executionId,
705
+ attemptId,
706
+ logicalCallId,
707
+ attemptGroup,
708
+ model,
709
+ estimatedInputTokens,
710
+ maxOutputTokens: maxTokens,
711
+ reservedUsd: estimatedWorstCaseUsd,
712
+ startedAt,
713
+ });
714
+ state.reservations.set(attemptId, {
715
+ attemptId,
716
+ logicalCallId,
717
+ attemptGroup,
718
+ reservedUsd: estimatedWorstCaseUsd,
719
+ });
720
+ try {
721
+ let result = {
722
+ streamOutcome: "truncated",
723
+ usage: null,
724
+ spoolPath: null,
725
+ upstreamStatus: null,
726
+ upstreamSource: null,
727
+ upstreamFailed: false,
728
+ };
729
+ try {
730
+ const upstream = await requestUpstream(
731
+ config.egressUrl,
732
+ incoming.url ?? "/",
733
+ incoming.method ?? "POST",
734
+ requestHeaders(incoming.headers, forwardedBody.length),
735
+ forwardedBody,
736
+ streamHardDeadlineMs,
737
+ );
738
+ const status = upstream.statusCode ?? 502;
739
+ const declaredSource = upstream.headers[egressSourceHeader];
740
+ const upstreamSource =
741
+ declaredSource === "provider" || declaredSource === "egress"
742
+ ? declaredSource
743
+ : null;
744
+ const forwarded =
745
+ rewritten.stream === true && status < 400
746
+ ? await forwardStreaming(upstream, outgoing, status, {
747
+ path: responseSpoolPath,
748
+ write: (bytes) => appendFileSync(responseSpoolPath, bytes),
749
+ close: () => undefined,
750
+ })
751
+ : await forwardNonStreaming(upstream, outgoing, status);
752
+ result = {
753
+ ...forwarded,
754
+ spoolPath: forwarded.spoolPath ?? null,
755
+ upstreamStatus: status,
756
+ upstreamSource: forwarded.upstreamFailed ? "egress" : upstreamSource,
757
+ };
758
+ } catch {
759
+ if (!outgoing.headersSent) {
760
+ sendJson(outgoing, 502, { error: "Egress request failed." });
761
+ } else if (!outgoing.destroyed) {
762
+ outgoing.destroy();
763
+ }
764
+ }
765
+
766
+ const usage = normalizeUsage(
767
+ result.usage,
768
+ estimatedInputTokens,
769
+ maxTokens,
770
+ );
771
+ const leaseChargeUsd = usageCharge(usage, pricing, estimatedWorstCaseUsd);
772
+ // The seven core fact fields are attemptId, logicalCallId, executionId,
773
+ // streamOutcome, usage, costUsd, and costIsEstimate. Other fields are spool metadata.
774
+ appendRow(spoolPath, {
775
+ kind: "request_attempt",
776
+ runId: config.runId,
777
+ caseId: config.caseId,
778
+ stepId,
779
+ executionId: config.executionId,
780
+ attemptId,
781
+ logicalCallId,
782
+ attemptGroup,
783
+ attribution: "ok",
784
+ model,
785
+ estimatedInputTokens,
786
+ maxOutputTokens: maxTokens,
787
+ streamOutcome: result.streamOutcome,
788
+ upstreamStatus: result.upstreamStatus,
789
+ upstreamSource: result.upstreamSource,
790
+ ...(result.finishedWithoutSentinel
791
+ ? { finishedWithoutSentinel: true }
792
+ : {}),
793
+ usage,
794
+ responseSpoolPath: result.spoolPath,
795
+ costUsd: leaseChargeUsd,
796
+ costIsEstimate: usage === null,
797
+ reservedUsd: estimatedWorstCaseUsd,
798
+ leaseChargeUsd,
799
+ startedAt,
800
+ endedAt: new Date().toISOString(),
801
+ });
802
+ state.reservations.delete(attemptId);
803
+ state.spentUsd += leaseChargeUsd;
804
+ if (!outgoing.destroyed && !outgoing.writableEnded) outgoing.end();
805
+ } finally {
806
+ reservedUsd -= estimatedWorstCaseUsd;
807
+ }
808
+ }
809
+
810
+ const server = createServer((incoming, outgoing) => {
811
+ void handle(incoming, outgoing).catch(() => {
812
+ if (!outgoing.headersSent) {
813
+ sendJson(outgoing, 500, { error: "Proxy request failed." });
814
+ } else if (!outgoing.destroyed) {
815
+ outgoing.destroy();
816
+ }
817
+ });
818
+ });
819
+
820
+ await new Promise((resolve, reject) => {
821
+ server.once("error", reject);
822
+ server.listen(config.port, config.host, resolve);
823
+ });
824
+ const address = server.address();
825
+ if (address === null || typeof address === "string") {
826
+ throw new Error("Proxy did not bind a TCP address");
827
+ }
828
+ process.stdout.write(
829
+ `${JSON.stringify({ event: "ready", host: config.host, port: address.port })}\n`,
830
+ );
831
+
832
+ const shutdown = () => server.close(() => process.exit(0));
833
+ process.once("SIGINT", shutdown);
834
+ process.once("SIGTERM", shutdown);
835
+ }
836
+
837
+ main().catch((error) => {
838
+ const message = error instanceof Error ? error.message : "unknown error";
839
+ process.stderr.write(`Proxy startup failed: ${message}\n`);
840
+ process.exitCode = 1;
841
+ });