@peterwangze/claude-trigger-router 1.10.0 → 1.12.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 +103 -460
- package/dist/cli.js +238 -20
- package/dist/cli.js.map +2 -2
- package/docs/release-notes-v1.11.0.md +36 -0
- package/docs/release-notes-v1.12.0.md +38 -0
- package/docs/releasing.md +5 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4298,18 +4298,26 @@ var init_SSEParser_transform = __esm({
|
|
|
4298
4298
|
"use strict";
|
|
4299
4299
|
SSEParserTransform = class {
|
|
4300
4300
|
buffer = "";
|
|
4301
|
+
currentEvent = {};
|
|
4302
|
+
decoder = new TextDecoder();
|
|
4301
4303
|
constructor() {
|
|
4302
4304
|
const transformStream = new TransformStream({
|
|
4303
4305
|
start: (controller) => {
|
|
4304
4306
|
},
|
|
4305
4307
|
transform: (chunk, controller) => {
|
|
4306
|
-
const text =
|
|
4308
|
+
const text = this.decoder.decode(chunk, { stream: true });
|
|
4307
4309
|
this.buffer += text;
|
|
4308
4310
|
this.parseBuffer(controller);
|
|
4309
4311
|
},
|
|
4310
4312
|
flush: (controller) => {
|
|
4313
|
+
const remaining = this.decoder.decode();
|
|
4314
|
+
if (remaining) {
|
|
4315
|
+
this.buffer += remaining;
|
|
4316
|
+
}
|
|
4311
4317
|
if (this.buffer.trim()) {
|
|
4312
|
-
this.parseBuffer(controller);
|
|
4318
|
+
this.parseBuffer(controller, true);
|
|
4319
|
+
} else if (Object.keys(this.currentEvent).length > 0) {
|
|
4320
|
+
this.parseBuffer(controller, true);
|
|
4313
4321
|
}
|
|
4314
4322
|
}
|
|
4315
4323
|
});
|
|
@@ -4318,25 +4326,29 @@ var init_SSEParser_transform = __esm({
|
|
|
4318
4326
|
}
|
|
4319
4327
|
readable;
|
|
4320
4328
|
writable;
|
|
4321
|
-
parseBuffer(controller) {
|
|
4322
|
-
const lines = this.buffer.split(
|
|
4323
|
-
this.buffer = lines.pop() || "";
|
|
4324
|
-
|
|
4325
|
-
|
|
4329
|
+
parseBuffer(controller, flush = false) {
|
|
4330
|
+
const lines = this.buffer.split(/\r?\n/);
|
|
4331
|
+
this.buffer = flush ? "" : lines.pop() || "";
|
|
4332
|
+
for (const rawLine of lines) {
|
|
4333
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
4326
4334
|
if (line.startsWith("event:")) {
|
|
4327
|
-
currentEvent.event = line.slice(6).trim();
|
|
4335
|
+
this.currentEvent.event = line.slice(6).trim();
|
|
4328
4336
|
} else if (line.startsWith("data:")) {
|
|
4329
4337
|
const dataStr = line.slice(5).trim();
|
|
4330
4338
|
try {
|
|
4331
|
-
currentEvent.data = JSON.parse(dataStr);
|
|
4339
|
+
this.currentEvent.data = JSON.parse(dataStr);
|
|
4332
4340
|
} catch {
|
|
4333
|
-
currentEvent.data = dataStr;
|
|
4341
|
+
this.currentEvent.data = dataStr;
|
|
4334
4342
|
}
|
|
4335
|
-
} else if (line === "" && Object.keys(currentEvent).length > 0) {
|
|
4336
|
-
controller.enqueue(currentEvent);
|
|
4337
|
-
currentEvent = {};
|
|
4343
|
+
} else if (line === "" && Object.keys(this.currentEvent).length > 0) {
|
|
4344
|
+
controller.enqueue(this.currentEvent);
|
|
4345
|
+
this.currentEvent = {};
|
|
4338
4346
|
}
|
|
4339
4347
|
}
|
|
4348
|
+
if (flush && Object.keys(this.currentEvent).length > 0) {
|
|
4349
|
+
controller.enqueue(this.currentEvent);
|
|
4350
|
+
this.currentEvent = {};
|
|
4351
|
+
}
|
|
4340
4352
|
}
|
|
4341
4353
|
};
|
|
4342
4354
|
}
|
|
@@ -4356,6 +4368,133 @@ function serializeEvent(event2) {
|
|
|
4356
4368
|
output3 += "\n";
|
|
4357
4369
|
return new TextEncoder().encode(output3);
|
|
4358
4370
|
}
|
|
4371
|
+
function serializeStreamErrorEvent(message) {
|
|
4372
|
+
return serializeEvent({
|
|
4373
|
+
event: "error",
|
|
4374
|
+
data: {
|
|
4375
|
+
type: "upstream_stream_error",
|
|
4376
|
+
message
|
|
4377
|
+
}
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
function parseSSEBlock(block) {
|
|
4381
|
+
const event2 = {};
|
|
4382
|
+
const dataLines = [];
|
|
4383
|
+
for (const rawLine of block.split(/\r?\n/)) {
|
|
4384
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
4385
|
+
if (line.startsWith("event:")) {
|
|
4386
|
+
event2.event = line.slice(6).trim();
|
|
4387
|
+
} else if (line.startsWith("data:")) {
|
|
4388
|
+
dataLines.push(line.slice(5).trim());
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
if (dataLines.length > 0) {
|
|
4392
|
+
const dataStr = dataLines.join("\n");
|
|
4393
|
+
try {
|
|
4394
|
+
event2.data = JSON.parse(dataStr);
|
|
4395
|
+
} catch {
|
|
4396
|
+
event2.data = dataStr;
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
return Object.keys(event2).length > 0 ? event2 : null;
|
|
4400
|
+
}
|
|
4401
|
+
function collectEventObservation(event2, observation) {
|
|
4402
|
+
const deltaText = event2?.data?.delta?.text;
|
|
4403
|
+
if (typeof deltaText === "string") {
|
|
4404
|
+
observation.sawText = true;
|
|
4405
|
+
observation.text += deltaText;
|
|
4406
|
+
}
|
|
4407
|
+
if (event2?.event === "message_delta" && event2?.data?.usage) {
|
|
4408
|
+
observation.usage = event2.data.usage;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
function observeSSEChunk(buffer, chunkText, observation) {
|
|
4412
|
+
let nextBuffer = buffer + chunkText;
|
|
4413
|
+
while (true) {
|
|
4414
|
+
const match = /\r?\n\r?\n/.exec(nextBuffer);
|
|
4415
|
+
if (!match || match.index < 0) {
|
|
4416
|
+
break;
|
|
4417
|
+
}
|
|
4418
|
+
const block = nextBuffer.slice(0, match.index);
|
|
4419
|
+
nextBuffer = nextBuffer.slice(match.index + match[0].length);
|
|
4420
|
+
const event2 = parseSSEBlock(block);
|
|
4421
|
+
if (event2) {
|
|
4422
|
+
collectEventObservation(event2, observation);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
return nextBuffer;
|
|
4426
|
+
}
|
|
4427
|
+
function finalizeStreamingTrace(req, observation) {
|
|
4428
|
+
if (req.sessionId && observation.usage) {
|
|
4429
|
+
sessionUsageCache.put(req.sessionId, observation.usage);
|
|
4430
|
+
}
|
|
4431
|
+
if (!req.governanceTrace) {
|
|
4432
|
+
return;
|
|
4433
|
+
}
|
|
4434
|
+
const outputGuardrail = inspectOutputGuardrail(observation.text);
|
|
4435
|
+
req.governanceTrace.outputGuardrail = outputGuardrail;
|
|
4436
|
+
for (const finding of outputGuardrail.findings) {
|
|
4437
|
+
appendTraceReason(req.governanceTrace, `output_guardrail:${finding.code}`);
|
|
4438
|
+
}
|
|
4439
|
+
if (observation.streamError) {
|
|
4440
|
+
appendTraceReason(req.governanceTrace, "upstream_stream_error");
|
|
4441
|
+
}
|
|
4442
|
+
req.governanceTrace.handoffSummary = summarizeRouteHandoffTrace(
|
|
4443
|
+
req.governanceTrace,
|
|
4444
|
+
getRuntimePipeline(req)
|
|
4445
|
+
);
|
|
4446
|
+
req.governanceTrace.spans = buildTraceSpansFromPipeline(
|
|
4447
|
+
req.governanceTrace,
|
|
4448
|
+
getRuntimePipeline(req)
|
|
4449
|
+
);
|
|
4450
|
+
req.governanceTrace = finalizeTrace(req.governanceTrace, {
|
|
4451
|
+
finalModel: req.body?.model ?? req.governanceTrace.finalModel
|
|
4452
|
+
});
|
|
4453
|
+
recordGovernanceTrace(req.governanceTrace);
|
|
4454
|
+
}
|
|
4455
|
+
function passThroughStreamingResponse(stream, req) {
|
|
4456
|
+
const decoder = new TextDecoder();
|
|
4457
|
+
const observation = { text: "", sawText: false };
|
|
4458
|
+
let buffer = "";
|
|
4459
|
+
let reader;
|
|
4460
|
+
return new ReadableStream({
|
|
4461
|
+
async start(controller) {
|
|
4462
|
+
reader = stream.getReader();
|
|
4463
|
+
try {
|
|
4464
|
+
while (true) {
|
|
4465
|
+
const { value, done } = await reader.read();
|
|
4466
|
+
if (done) {
|
|
4467
|
+
break;
|
|
4468
|
+
}
|
|
4469
|
+
controller.enqueue(value);
|
|
4470
|
+
buffer = observeSSEChunk(buffer, decoder.decode(value, { stream: true }), observation);
|
|
4471
|
+
}
|
|
4472
|
+
} catch (error) {
|
|
4473
|
+
const message = error instanceof Error && error.message ? error.message : "The upstream stream closed before completion.";
|
|
4474
|
+
observation.streamError = message;
|
|
4475
|
+
controller.enqueue(serializeStreamErrorEvent("The upstream stream closed before completion."));
|
|
4476
|
+
} finally {
|
|
4477
|
+
const remaining = decoder.decode();
|
|
4478
|
+
if (remaining) {
|
|
4479
|
+
buffer = observeSSEChunk(buffer, remaining, observation);
|
|
4480
|
+
}
|
|
4481
|
+
if (buffer.trim()) {
|
|
4482
|
+
const event2 = parseSSEBlock(buffer);
|
|
4483
|
+
if (event2) {
|
|
4484
|
+
collectEventObservation(event2, observation);
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
finalizeStreamingTrace(req, observation);
|
|
4488
|
+
reader.releaseLock();
|
|
4489
|
+
reader = void 0;
|
|
4490
|
+
controller.close();
|
|
4491
|
+
}
|
|
4492
|
+
},
|
|
4493
|
+
cancel(reason) {
|
|
4494
|
+
return reader?.cancel(reason);
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
}
|
|
4359
4498
|
async function collectSSE(stream) {
|
|
4360
4499
|
const parser = new SSEParserTransform();
|
|
4361
4500
|
const parsedStream = stream.pipeThrough(parser);
|
|
@@ -4395,6 +4534,12 @@ function governStreamingResponse(stream, req, config, servicePort, deps) {
|
|
|
4395
4534
|
to: resolveModelReference(config, level.to) ?? level.to
|
|
4396
4535
|
}))
|
|
4397
4536
|
} : void 0;
|
|
4537
|
+
const shouldBufferForStreamGuard = Boolean(
|
|
4538
|
+
config.Governance?.enabled && resolvedCascadeConfig?.enabled && resolvedCascadeConfig.stream_guard && req.governanceTrace
|
|
4539
|
+
);
|
|
4540
|
+
if (!shouldBufferForStreamGuard) {
|
|
4541
|
+
return passThroughStreamingResponse(stream, req);
|
|
4542
|
+
}
|
|
4398
4543
|
return new ReadableStream({
|
|
4399
4544
|
async start(controller) {
|
|
4400
4545
|
try {
|
|
@@ -11552,6 +11697,25 @@ var init_protocols = __esm({
|
|
|
11552
11697
|
});
|
|
11553
11698
|
|
|
11554
11699
|
// src/index.ts
|
|
11700
|
+
function getShutdownSignalHandlers() {
|
|
11701
|
+
const store = globalThis;
|
|
11702
|
+
if (!store[SHUTDOWN_SIGNAL_HANDLERS_KEY]) {
|
|
11703
|
+
store[SHUTDOWN_SIGNAL_HANDLERS_KEY] = [];
|
|
11704
|
+
}
|
|
11705
|
+
return store[SHUTDOWN_SIGNAL_HANDLERS_KEY];
|
|
11706
|
+
}
|
|
11707
|
+
function registerShutdownSignalHandlers(shutdown) {
|
|
11708
|
+
const handlers = getShutdownSignalHandlers();
|
|
11709
|
+
for (const { signal, handler } of handlers) {
|
|
11710
|
+
process.off(signal, handler);
|
|
11711
|
+
}
|
|
11712
|
+
handlers.length = 0;
|
|
11713
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
11714
|
+
const handler = () => shutdown(signal);
|
|
11715
|
+
process.on(signal, handler);
|
|
11716
|
+
handlers.push({ signal, handler });
|
|
11717
|
+
}
|
|
11718
|
+
}
|
|
11555
11719
|
function cloneRequestBody(value) {
|
|
11556
11720
|
if (typeof structuredClone === "function") {
|
|
11557
11721
|
return structuredClone(value);
|
|
@@ -11626,6 +11790,37 @@ function buildRemoteForwardHeaders(req, authToken) {
|
|
|
11626
11790
|
headers["x-ctr-remote-forward"] = "1";
|
|
11627
11791
|
return headers;
|
|
11628
11792
|
}
|
|
11793
|
+
function isSSEContentType(contentType) {
|
|
11794
|
+
return Boolean(contentType?.toLowerCase().includes("text/event-stream"));
|
|
11795
|
+
}
|
|
11796
|
+
function clearTimerOnStreamClose(stream, clear) {
|
|
11797
|
+
let reader;
|
|
11798
|
+
return new ReadableStream({
|
|
11799
|
+
async start(controller) {
|
|
11800
|
+
reader = stream.getReader();
|
|
11801
|
+
try {
|
|
11802
|
+
while (true) {
|
|
11803
|
+
const { value, done } = await reader.read();
|
|
11804
|
+
if (done) {
|
|
11805
|
+
break;
|
|
11806
|
+
}
|
|
11807
|
+
controller.enqueue(value);
|
|
11808
|
+
}
|
|
11809
|
+
controller.close();
|
|
11810
|
+
} catch (error) {
|
|
11811
|
+
controller.error(error);
|
|
11812
|
+
} finally {
|
|
11813
|
+
clear();
|
|
11814
|
+
reader?.releaseLock();
|
|
11815
|
+
reader = void 0;
|
|
11816
|
+
}
|
|
11817
|
+
},
|
|
11818
|
+
cancel(reason) {
|
|
11819
|
+
clear();
|
|
11820
|
+
return reader?.cancel(reason);
|
|
11821
|
+
}
|
|
11822
|
+
});
|
|
11823
|
+
}
|
|
11629
11824
|
async function forwardModelCallToRemote(req, reply, config) {
|
|
11630
11825
|
if (!isModelCallPath(req.url) || !isRemoteForwardEnabled(config) || req.headers?.["x-ctr-remote-forward"] === "1") {
|
|
11631
11826
|
return false;
|
|
@@ -11635,12 +11830,28 @@ async function forwardModelCallToRemote(req, reply, config) {
|
|
|
11635
11830
|
const forwardPath = getRemoteForwardPath(req.url);
|
|
11636
11831
|
const path = forwardPath.split("?")[0];
|
|
11637
11832
|
const targetUrl = `${remoteBaseUrl}${forwardPath}`;
|
|
11833
|
+
const abortController = new AbortController();
|
|
11834
|
+
let timeout;
|
|
11835
|
+
const clearRemoteTimeout = () => {
|
|
11836
|
+
if (timeout) {
|
|
11837
|
+
clearTimeout(timeout);
|
|
11838
|
+
timeout = void 0;
|
|
11839
|
+
}
|
|
11840
|
+
};
|
|
11841
|
+
timeout = setTimeout(() => {
|
|
11842
|
+
abortController.abort(new Error("remote_forward_timeout"));
|
|
11843
|
+
}, config.API_TIMEOUT_MS ?? 6e5);
|
|
11844
|
+
const abortOnClientClose = () => {
|
|
11845
|
+
clearRemoteTimeout();
|
|
11846
|
+
abortController.abort(new Error("client_connection_closed"));
|
|
11847
|
+
};
|
|
11848
|
+
reply.raw?.once?.("close", abortOnClientClose);
|
|
11638
11849
|
try {
|
|
11639
11850
|
const response = await fetch(targetUrl, {
|
|
11640
11851
|
method: String(req.method ?? "POST").toUpperCase(),
|
|
11641
11852
|
headers: buildRemoteForwardHeaders(req, remoteService.auth_token),
|
|
11642
11853
|
body: req.body === void 0 ? void 0 : JSON.stringify(req.body),
|
|
11643
|
-
signal:
|
|
11854
|
+
signal: abortController.signal
|
|
11644
11855
|
});
|
|
11645
11856
|
reply.code(response.status);
|
|
11646
11857
|
const contentType = response.headers.get("content-type");
|
|
@@ -11654,9 +11865,11 @@ async function forwardModelCallToRemote(req, reply, config) {
|
|
|
11654
11865
|
req.remoteForwarded = true;
|
|
11655
11866
|
req.responseGovernanceApplied = true;
|
|
11656
11867
|
if (response.body) {
|
|
11657
|
-
|
|
11868
|
+
const body = clearTimerOnStreamClose(response.body, clearRemoteTimeout);
|
|
11869
|
+
reply.send(isSSEContentType(contentType) ? governStreamingResponse(body, req, config, config.PORT ?? 5678) : body);
|
|
11658
11870
|
return true;
|
|
11659
11871
|
}
|
|
11872
|
+
clearRemoteTimeout();
|
|
11660
11873
|
reply.send(response.ok ? {} : { error: `Remote service returned HTTP ${response.status}` });
|
|
11661
11874
|
return true;
|
|
11662
11875
|
} catch (error) {
|
|
@@ -11673,6 +11886,10 @@ async function forwardModelCallToRemote(req, reply, config) {
|
|
|
11673
11886
|
}
|
|
11674
11887
|
});
|
|
11675
11888
|
return true;
|
|
11889
|
+
} finally {
|
|
11890
|
+
if (!req.remoteForwarded) {
|
|
11891
|
+
clearRemoteTimeout();
|
|
11892
|
+
}
|
|
11676
11893
|
}
|
|
11677
11894
|
}
|
|
11678
11895
|
async function run(options = {}) {
|
|
@@ -11722,8 +11939,7 @@ async function run(options = {}) {
|
|
|
11722
11939
|
process.exit(0);
|
|
11723
11940
|
});
|
|
11724
11941
|
};
|
|
11725
|
-
|
|
11726
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
11942
|
+
registerShutdownSignalHandlers(shutdown);
|
|
11727
11943
|
const servicePort = process.env.SERVICE_PORT ? parseInt(process.env.SERVICE_PORT) : port;
|
|
11728
11944
|
config.PORT = servicePort;
|
|
11729
11945
|
const pad = (num) => (num > 9 ? "" : "0") + num;
|
|
@@ -12176,7 +12392,7 @@ async function run(options = {}) {
|
|
|
12176
12392
|
}).then((governedPayload) => {
|
|
12177
12393
|
req.responseGovernanceApplied = true;
|
|
12178
12394
|
if (governedPayload && typeof governedPayload === "object" && governedPayload.error) {
|
|
12179
|
-
return done(
|
|
12395
|
+
return done(null, governedPayload);
|
|
12180
12396
|
}
|
|
12181
12397
|
if (req.sessionId && governedPayload?.usage) {
|
|
12182
12398
|
sessionUsageCache.put(req.sessionId, governedPayload.usage);
|
|
@@ -12185,7 +12401,7 @@ async function run(options = {}) {
|
|
|
12185
12401
|
}).catch((error) => done(error, null));
|
|
12186
12402
|
return;
|
|
12187
12403
|
}
|
|
12188
|
-
return done(
|
|
12404
|
+
return done(null, payload);
|
|
12189
12405
|
}
|
|
12190
12406
|
done(null, payload);
|
|
12191
12407
|
});
|
|
@@ -12215,7 +12431,7 @@ async function run(options = {}) {
|
|
|
12215
12431
|
});
|
|
12216
12432
|
await server.start();
|
|
12217
12433
|
}
|
|
12218
|
-
var import_fs8, import_promises5, import_os2, import_path8, import_json5, import_node_events, import_rotating_file_stream, event;
|
|
12434
|
+
var import_fs8, import_promises5, import_os2, import_path8, import_json5, import_node_events, import_rotating_file_stream, event, SHUTDOWN_SIGNAL_HANDLERS_KEY, SHUTDOWN_SIGNALS;
|
|
12219
12435
|
var init_index = __esm({
|
|
12220
12436
|
"src/index.ts"() {
|
|
12221
12437
|
"use strict";
|
|
@@ -12249,6 +12465,8 @@ var init_index = __esm({
|
|
|
12249
12465
|
init_protocols();
|
|
12250
12466
|
init_pipeline();
|
|
12251
12467
|
event = new import_node_events.EventEmitter();
|
|
12468
|
+
SHUTDOWN_SIGNAL_HANDLERS_KEY = Symbol.for("claude-trigger-router.shutdownSignalHandlers");
|
|
12469
|
+
SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
12252
12470
|
}
|
|
12253
12471
|
});
|
|
12254
12472
|
|