@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 +4 -4
- package/dist/cli.js +108 -19
- package/dist/cli.js.map +2 -2
- package/docs/release-notes-v1.12.0.md +38 -0
- package/docs/releasing.md +4 -4
- package/package.json +1 -1
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.
|
|
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.
|
|
224
|
+
## v1.12.0 发布定位
|
|
225
225
|
|
|
226
|
-
`v1.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 = {}) {
|