@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/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 = new TextDecoder().decode(chunk);
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("\n");
4323
- this.buffer = lines.pop() || "";
4324
- let currentEvent = {};
4325
- for (const line of lines) {
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: AbortSignal.timeout(config.API_TIMEOUT_MS ?? 6e5)
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
- reply.send(response.body);
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
- process.on("SIGINT", () => shutdown("SIGINT"));
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(governedPayload.error, null);
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(payload.error, null);
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