@peterwangze/claude-trigger-router 1.11.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 CHANGED
@@ -218,14 +218,14 @@ ANTHROPIC_AUTH_TOKEN=<managed-key>
218
218
  - 配置指南:[docs/configuration-guide.md](docs/configuration-guide.md)
219
219
  - Models 迁移:[docs/models-migration-guide.md](docs/models-migration-guide.md)
220
220
  - CLI 测试矩阵:[docs/cli-test-matrix.md](docs/cli-test-matrix.md)
221
- - 发布说明:[docs/release-notes-v1.11.0.md](docs/release-notes-v1.11.0.md)
221
+ - 发布说明:[docs/release-notes-v1.12.0.md](docs/release-notes-v1.12.0.md)
222
222
  - 发布验证:[docs/releasing.md](docs/releasing.md)
223
223
 
224
- ## v1.11.0 发布定位
224
+ ## v1.12.0 发布定位
225
225
 
226
- `v1.11.0` 是基础路由流式稳定性与 socket 错误修复版。它修复 `v1.8.0` 之后流式 response governance 默认全量缓冲导致的基础路由输出中断,并让上游结构化 API error 稳定返回,不再被转换成 socket-level hook error。
226
+ `v1.12.0` 是流式传输韧性与远程中转稳定性修复版。它在 `v1.11.0` 首轮止血基础上继续修复用户复现的 socket 断连和中转卡顿:上游流式中途断开时返回可读 SSE error event,远程中转会在客户端断开时主动取消上游请求,并修复 SSE parser 在多字节字符跨 chunk 时的解析风险。
227
227
 
228
- 这个版本不新增 SmartRouter 协作模式、不改变远程客户端配置心智;`v1.10.0` 的 routing advisor、confidence threshold、latency budget 和 collaboration contract 继续保留。完整发布边界见 [docs/release-notes-v1.11.0.md](docs/release-notes-v1.11.0.md)。
228
+ 这个版本不新增 SmartRouter 协作模式、不改变远程客户端配置心智;`v1.10.0` 的 routing advisor、confidence threshold、latency budget 和 collaboration contract 继续保留。完整发布边界见 [docs/release-notes-v1.12.0.md](docs/release-notes-v1.12.0.md)。
229
229
 
230
230
  ## License
231
231
 
package/dist/cli.js CHANGED
@@ -4299,16 +4299,21 @@ var init_SSEParser_transform = __esm({
4299
4299
  SSEParserTransform = class {
4300
4300
  buffer = "";
4301
4301
  currentEvent = {};
4302
+ decoder = new TextDecoder();
4302
4303
  constructor() {
4303
4304
  const transformStream = new TransformStream({
4304
4305
  start: (controller) => {
4305
4306
  },
4306
4307
  transform: (chunk, controller) => {
4307
- const text = new TextDecoder().decode(chunk);
4308
+ const text = this.decoder.decode(chunk, { stream: true });
4308
4309
  this.buffer += text;
4309
4310
  this.parseBuffer(controller);
4310
4311
  },
4311
4312
  flush: (controller) => {
4313
+ const remaining = this.decoder.decode();
4314
+ if (remaining) {
4315
+ this.buffer += remaining;
4316
+ }
4312
4317
  if (this.buffer.trim()) {
4313
4318
  this.parseBuffer(controller, true);
4314
4319
  } else if (Object.keys(this.currentEvent).length > 0) {
@@ -4363,6 +4368,15 @@ function serializeEvent(event2) {
4363
4368
  output3 += "\n";
4364
4369
  return new TextEncoder().encode(output3);
4365
4370
  }
4371
+ function serializeStreamErrorEvent(message) {
4372
+ return serializeEvent({
4373
+ event: "error",
4374
+ data: {
4375
+ type: "upstream_stream_error",
4376
+ message
4377
+ }
4378
+ });
4379
+ }
4366
4380
  function parseSSEBlock(block) {
4367
4381
  const event2 = {};
4368
4382
  const dataLines = [];
@@ -4422,6 +4436,9 @@ function finalizeStreamingTrace(req, observation) {
4422
4436
  for (const finding of outputGuardrail.findings) {
4423
4437
  appendTraceReason(req.governanceTrace, `output_guardrail:${finding.code}`);
4424
4438
  }
4439
+ if (observation.streamError) {
4440
+ appendTraceReason(req.governanceTrace, "upstream_stream_error");
4441
+ }
4425
4442
  req.governanceTrace.handoffSummary = summarizeRouteHandoffTrace(
4426
4443
  req.governanceTrace,
4427
4444
  getRuntimePipeline(req)
@@ -4439,25 +4456,44 @@ function passThroughStreamingResponse(stream, req) {
4439
4456
  const decoder = new TextDecoder();
4440
4457
  const observation = { text: "", sawText: false };
4441
4458
  let buffer = "";
4442
- return stream.pipeThrough(new TransformStream({
4443
- transform(chunk, controller) {
4444
- controller.enqueue(chunk);
4445
- buffer = observeSSEChunk(buffer, decoder.decode(chunk, { stream: true }), observation);
4446
- },
4447
- flush() {
4448
- const remaining = decoder.decode();
4449
- if (remaining) {
4450
- buffer = observeSSEChunk(buffer, remaining, observation);
4451
- }
4452
- if (buffer.trim()) {
4453
- const event2 = parseSSEBlock(buffer);
4454
- if (event2) {
4455
- collectEventObservation(event2, observation);
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);
4456
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();
4457
4491
  }
4458
- finalizeStreamingTrace(req, observation);
4492
+ },
4493
+ cancel(reason) {
4494
+ return reader?.cancel(reason);
4459
4495
  }
4460
- }));
4496
+ });
4461
4497
  }
4462
4498
  async function collectSSE(stream) {
4463
4499
  const parser = new SSEParserTransform();
@@ -11754,6 +11790,37 @@ function buildRemoteForwardHeaders(req, authToken) {
11754
11790
  headers["x-ctr-remote-forward"] = "1";
11755
11791
  return headers;
11756
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
+ }
11757
11824
  async function forwardModelCallToRemote(req, reply, config) {
11758
11825
  if (!isModelCallPath(req.url) || !isRemoteForwardEnabled(config) || req.headers?.["x-ctr-remote-forward"] === "1") {
11759
11826
  return false;
@@ -11763,12 +11830,28 @@ async function forwardModelCallToRemote(req, reply, config) {
11763
11830
  const forwardPath = getRemoteForwardPath(req.url);
11764
11831
  const path = forwardPath.split("?")[0];
11765
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);
11766
11849
  try {
11767
11850
  const response = await fetch(targetUrl, {
11768
11851
  method: String(req.method ?? "POST").toUpperCase(),
11769
11852
  headers: buildRemoteForwardHeaders(req, remoteService.auth_token),
11770
11853
  body: req.body === void 0 ? void 0 : JSON.stringify(req.body),
11771
- signal: AbortSignal.timeout(config.API_TIMEOUT_MS ?? 6e5)
11854
+ signal: abortController.signal
11772
11855
  });
11773
11856
  reply.code(response.status);
11774
11857
  const contentType = response.headers.get("content-type");
@@ -11782,9 +11865,11 @@ async function forwardModelCallToRemote(req, reply, config) {
11782
11865
  req.remoteForwarded = true;
11783
11866
  req.responseGovernanceApplied = true;
11784
11867
  if (response.body) {
11785
- reply.send(response.body);
11868
+ const body = clearTimerOnStreamClose(response.body, clearRemoteTimeout);
11869
+ reply.send(isSSEContentType(contentType) ? governStreamingResponse(body, req, config, config.PORT ?? 5678) : body);
11786
11870
  return true;
11787
11871
  }
11872
+ clearRemoteTimeout();
11788
11873
  reply.send(response.ok ? {} : { error: `Remote service returned HTTP ${response.status}` });
11789
11874
  return true;
11790
11875
  } catch (error) {
@@ -11801,6 +11886,10 @@ async function forwardModelCallToRemote(req, reply, config) {
11801
11886
  }
11802
11887
  });
11803
11888
  return true;
11889
+ } finally {
11890
+ if (!req.remoteForwarded) {
11891
+ clearRemoteTimeout();
11892
+ }
11804
11893
  }
11805
11894
  }
11806
11895
  async function run(options = {}) {