@peterwangze/claude-trigger-router 1.11.0 → 1.13.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 +11 -7
- package/config/trigger.smart-router.advanced.yaml +214 -0
- package/config/trigger.smart-router.yaml +21 -138
- package/dist/cli.js +466 -19
- package/dist/cli.js.map +4 -4
- package/docs/configuration-guide.md +7 -2
- package/docs/release-notes-v1.12.0.md +38 -0
- package/docs/release-notes-v1.13.0.md +49 -0
- package/docs/releasing.md +12 -4
- package/package.json +2 -1
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
|
-
|
|
4455
|
-
|
|
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
|
+
}
|
|
4456
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 = {}) {
|
|
@@ -13970,6 +14059,8 @@ function createDefaultDeps(io = createConsoleIO()) {
|
|
|
13970
14059
|
}
|
|
13971
14060
|
function printRoutingNextSteps(io) {
|
|
13972
14061
|
io.info("\u4F60\u53EF\u4EE5\u6309\u9700\u7EE7\u7EED\u914D\u7F6E\u8DEF\u7531\u80FD\u529B\uFF1A");
|
|
14062
|
+
io.info(' - \u5148\u8FD0\u884C ctr doctor --route-preview --route-text "\u4F60\u7684\u8BF7\u6C42"\uFF0C\u786E\u8BA4\u672C\u6B21\u4F1A\u547D\u4E2D\u54EA\u4E2A\u69FD\u4F4D\u6216 SmartRouter \u8DEF\u5F84');
|
|
14063
|
+
io.info(" - \u57FA\u7840\u8DEF\u7531\u987A\u5E8F\uFF1A\u663E\u5F0F\u4E0A\u6E38\u6A21\u578B -> longContext -> background -> think -> webSearch -> default");
|
|
13973
14064
|
io.info(" - SmartRouter.rules\uFF1A\u9002\u5408\u9AD8\u786E\u5B9A\u6027\u4EFB\u52A1\uFF0C\u628A\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u7B49\u8BF7\u6C42\u56FA\u5B9A\u5207\u5230\u6307\u5B9A\u6A21\u578B");
|
|
13974
14065
|
io.info(" - SmartRouter candidates\uFF1A\u9002\u5408\u6A21\u7CCA\u4EFB\u52A1\uFF0C\u5728\u5019\u9009\u6A21\u578B\u4E4B\u95F4\u81EA\u52A8\u9009\u62E9\u66F4\u5408\u9002\u7684\u6A21\u578B");
|
|
13975
14066
|
io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
|
|
@@ -14155,6 +14246,294 @@ var init_setup2 = __esm({
|
|
|
14155
14246
|
}
|
|
14156
14247
|
});
|
|
14157
14248
|
|
|
14249
|
+
// src/router/route-preview.ts
|
|
14250
|
+
function resolveDisplayModel(config, ref) {
|
|
14251
|
+
if (!ref) {
|
|
14252
|
+
return {};
|
|
14253
|
+
}
|
|
14254
|
+
const resolved = resolveModelReference(config, ref);
|
|
14255
|
+
return {
|
|
14256
|
+
model: ref,
|
|
14257
|
+
resolved
|
|
14258
|
+
};
|
|
14259
|
+
}
|
|
14260
|
+
function extractRuleMatch(text, rules) {
|
|
14261
|
+
const sortedRules = rules.filter((rule) => rule.enabled !== false).sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
14262
|
+
for (const rule of sortedRules) {
|
|
14263
|
+
for (const pattern of rule.patterns ?? []) {
|
|
14264
|
+
const result = patternMatcher.match(text, pattern);
|
|
14265
|
+
if (!result.matched) {
|
|
14266
|
+
continue;
|
|
14267
|
+
}
|
|
14268
|
+
const detail = result.matchedKeyword ? `keyword "${result.matchedKeyword}"` : result.regexMatch ? `regex "${pattern.pattern}"` : pattern.type;
|
|
14269
|
+
return { rule, detail };
|
|
14270
|
+
}
|
|
14271
|
+
}
|
|
14272
|
+
return void 0;
|
|
14273
|
+
}
|
|
14274
|
+
function buildSemanticCandidates(rules, config) {
|
|
14275
|
+
const defaultThreshold = config.semantic?.threshold;
|
|
14276
|
+
const prototypes = config.semantic?.prototypes ?? {};
|
|
14277
|
+
return rules.filter((rule) => rule.enabled !== false).map((rule) => {
|
|
14278
|
+
const prototype = rule.semantic_profile?.prototype ?? prototypes[rule.name] ?? rule.description;
|
|
14279
|
+
if (!prototype || rule.semantic_profile?.enabled === false) {
|
|
14280
|
+
return void 0;
|
|
14281
|
+
}
|
|
14282
|
+
return {
|
|
14283
|
+
intent: rule.name,
|
|
14284
|
+
prototype,
|
|
14285
|
+
threshold: rule.semantic_profile?.threshold ?? defaultThreshold,
|
|
14286
|
+
rule
|
|
14287
|
+
};
|
|
14288
|
+
}).filter((item) => Boolean(item));
|
|
14289
|
+
}
|
|
14290
|
+
function previewBasicRouter(config, input3) {
|
|
14291
|
+
const steps = [];
|
|
14292
|
+
const warnings = [];
|
|
14293
|
+
const registry = buildModelRegistry(config);
|
|
14294
|
+
const tokenCount = input3.tokenCount ?? 0;
|
|
14295
|
+
const longContextThreshold = config.Router.longContextThreshold || 6e4;
|
|
14296
|
+
const explicit = resolveModelReference(config, input3.model);
|
|
14297
|
+
if (explicit && explicit.includes(",")) {
|
|
14298
|
+
steps.push({
|
|
14299
|
+
label: "Explicit model",
|
|
14300
|
+
status: "matched",
|
|
14301
|
+
detail: `\u8BF7\u6C42\u6A21\u578B "${input3.model}" \u5DF2\u89E3\u6790\u4E3A\u4E0A\u6E38\u5F15\u7528\uFF0C\u57FA\u7840\u69FD\u4F4D\u4E0D\u4F1A\u518D\u8986\u76D6\u3002`
|
|
14302
|
+
});
|
|
14303
|
+
return {
|
|
14304
|
+
input: input3,
|
|
14305
|
+
finalModel: explicit,
|
|
14306
|
+
finalModelRef: input3.model,
|
|
14307
|
+
source: "explicit_model",
|
|
14308
|
+
steps,
|
|
14309
|
+
warnings
|
|
14310
|
+
};
|
|
14311
|
+
}
|
|
14312
|
+
if (tokenCount > longContextThreshold && config.Router.longContext) {
|
|
14313
|
+
const model = resolveDisplayModel(config, config.Router.longContext);
|
|
14314
|
+
steps.push({
|
|
14315
|
+
label: "Router.longContext",
|
|
14316
|
+
status: "matched",
|
|
14317
|
+
detail: `tokenCount ${tokenCount} > threshold ${longContextThreshold}\uFF0C\u4F18\u5148\u4F7F\u7528\u957F\u4E0A\u4E0B\u6587\u69FD\u4F4D\u3002`
|
|
14318
|
+
});
|
|
14319
|
+
return {
|
|
14320
|
+
input: input3,
|
|
14321
|
+
finalModel: model.resolved,
|
|
14322
|
+
finalModelRef: model.model,
|
|
14323
|
+
source: "basic_long_context",
|
|
14324
|
+
steps,
|
|
14325
|
+
warnings
|
|
14326
|
+
};
|
|
14327
|
+
}
|
|
14328
|
+
steps.push({
|
|
14329
|
+
label: "Router.longContext",
|
|
14330
|
+
status: config.Router.longContext ? "info" : "skipped",
|
|
14331
|
+
detail: config.Router.longContext ? `\u5F53\u524D tokenCount ${tokenCount} \u672A\u8D85\u8FC7 threshold ${longContextThreshold}\uFF1B\u771F\u5B9E\u8FD0\u884C\u8FD8\u4F1A\u5728\u6700\u7EC8\u5B9A\u6A21\u540E\u68C0\u67E5 safe_input/context_window\u3002` : "\u672A\u914D\u7F6E Router.longContext\uFF0C\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u4F1A\u56DE\u5230\u5F53\u524D\u9009\u4E2D\u6A21\u578B\u3002"
|
|
14332
|
+
});
|
|
14333
|
+
if (input3.model?.startsWith("claude-3-5-haiku") && config.Router.background) {
|
|
14334
|
+
const model = resolveDisplayModel(config, config.Router.background);
|
|
14335
|
+
steps.push({
|
|
14336
|
+
label: "Router.background",
|
|
14337
|
+
status: "matched",
|
|
14338
|
+
detail: "\u8BF7\u6C42\u6A21\u578B\u4EE5 claude-3-5-haiku \u5F00\u5934\uFF0C\u6309\u5F53\u524D\u57FA\u7840\u8DEF\u7531\u5B9E\u73B0\u8BC6\u522B\u4E3A\u540E\u53F0\u8BF7\u6C42\u3002"
|
|
14339
|
+
});
|
|
14340
|
+
return {
|
|
14341
|
+
input: input3,
|
|
14342
|
+
finalModel: model.resolved,
|
|
14343
|
+
finalModelRef: model.model,
|
|
14344
|
+
source: "basic_background",
|
|
14345
|
+
steps,
|
|
14346
|
+
warnings
|
|
14347
|
+
};
|
|
14348
|
+
}
|
|
14349
|
+
steps.push({
|
|
14350
|
+
label: "Router.background",
|
|
14351
|
+
status: config.Router.background ? "info" : "skipped",
|
|
14352
|
+
detail: config.Router.background ? "\u4EC5\u5F53\u8BF7\u6C42\u6A21\u578B\u4EE5 claude-3-5-haiku \u5F00\u5934\u65F6\u547D\u4E2D\uFF1B\u5982\u679C Claude Code \u540E\u53F0\u6A21\u578B\u6807\u8BC6\u53D8\u5316\uFF0C\u9700\u8981\u91CD\u65B0\u6821\u51C6\u3002" : "\u672A\u914D\u7F6E Router.background\u3002"
|
|
14353
|
+
});
|
|
14354
|
+
if (input3.thinking && config.Router.think) {
|
|
14355
|
+
const model = resolveDisplayModel(config, config.Router.think);
|
|
14356
|
+
steps.push({
|
|
14357
|
+
label: "Router.think",
|
|
14358
|
+
status: "matched",
|
|
14359
|
+
detail: "\u8BF7\u6C42\u5305\u542B thinking\uFF0C\u4F7F\u7528\u601D\u8003\u69FD\u4F4D\u3002"
|
|
14360
|
+
});
|
|
14361
|
+
return {
|
|
14362
|
+
input: input3,
|
|
14363
|
+
finalModel: model.resolved,
|
|
14364
|
+
finalModelRef: model.model,
|
|
14365
|
+
source: "basic_thinking",
|
|
14366
|
+
steps,
|
|
14367
|
+
warnings
|
|
14368
|
+
};
|
|
14369
|
+
}
|
|
14370
|
+
steps.push({
|
|
14371
|
+
label: "Router.think",
|
|
14372
|
+
status: config.Router.think ? "info" : "skipped",
|
|
14373
|
+
detail: config.Router.think ? "\u672C\u6B21\u672A\u58F0\u660E thinking\uFF1B\u5982\u679C\u540C\u65F6\u8D85\u8FC7 longContext threshold\uFF0C\u957F\u4E0A\u4E0B\u6587\u4F1A\u5148\u4E8E thinking \u547D\u4E2D\u3002" : "\u672A\u914D\u7F6E Router.think\u3002"
|
|
14374
|
+
});
|
|
14375
|
+
if (input3.webSearch && config.Router.webSearch) {
|
|
14376
|
+
const model = resolveDisplayModel(config, config.Router.webSearch);
|
|
14377
|
+
steps.push({
|
|
14378
|
+
label: "Router.webSearch",
|
|
14379
|
+
status: "matched",
|
|
14380
|
+
detail: "\u8BF7\u6C42\u5305\u542B web_search \u5DE5\u5177\uFF0C\u4F7F\u7528\u8054\u7F51\u641C\u7D22\u69FD\u4F4D\u3002"
|
|
14381
|
+
});
|
|
14382
|
+
return {
|
|
14383
|
+
input: input3,
|
|
14384
|
+
finalModel: model.resolved,
|
|
14385
|
+
finalModelRef: model.model,
|
|
14386
|
+
source: "basic_web_search",
|
|
14387
|
+
steps,
|
|
14388
|
+
warnings
|
|
14389
|
+
};
|
|
14390
|
+
}
|
|
14391
|
+
steps.push({
|
|
14392
|
+
label: "Router.webSearch",
|
|
14393
|
+
status: config.Router.webSearch ? "info" : "skipped",
|
|
14394
|
+
detail: config.Router.webSearch ? "\u672C\u6B21\u672A\u58F0\u660E web_search\uFF1B\u5982\u679C\u540C\u65F6\u8D85\u8FC7 longContext threshold\uFF0C\u957F\u4E0A\u4E0B\u6587\u4F1A\u5148\u4E8E webSearch \u547D\u4E2D\u3002" : "\u672A\u914D\u7F6E Router.webSearch\u3002"
|
|
14395
|
+
});
|
|
14396
|
+
const defaultModel = resolveDisplayModel(config, config.Router.default);
|
|
14397
|
+
if (!defaultModel.resolved || !getCompiledModelRef(config, defaultModel.resolved)) {
|
|
14398
|
+
warnings.push(`Router.default "${config.Router.default}" \u672A\u89E3\u6790\u5230\u53EF\u7528\u6A21\u578B\u3002`);
|
|
14399
|
+
}
|
|
14400
|
+
steps.push({
|
|
14401
|
+
label: "Router.default",
|
|
14402
|
+
status: defaultModel.resolved ? "matched" : "warning",
|
|
14403
|
+
detail: defaultModel.resolved ? "\u524D\u7F6E\u69FD\u4F4D\u5747\u672A\u547D\u4E2D\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u6A21\u578B\u3002" : "Router.default \u672A\u89E3\u6790\u5230\u53EF\u7528\u6A21\u578B\u3002"
|
|
14404
|
+
});
|
|
14405
|
+
if (defaultModel.model && !registry.modelMap[defaultModel.model] && !defaultModel.model.includes(",")) {
|
|
14406
|
+
warnings.push(`Router.default "${defaultModel.model}" \u4E0D\u5728 Models/Providers/Registration \u4E2D\u3002`);
|
|
14407
|
+
}
|
|
14408
|
+
return {
|
|
14409
|
+
input: input3,
|
|
14410
|
+
finalModel: defaultModel.resolved,
|
|
14411
|
+
finalModelRef: defaultModel.model,
|
|
14412
|
+
source: defaultModel.resolved ? "basic_default" : "unresolved",
|
|
14413
|
+
steps,
|
|
14414
|
+
warnings
|
|
14415
|
+
};
|
|
14416
|
+
}
|
|
14417
|
+
function previewRoute(config, input3) {
|
|
14418
|
+
const smartRouterConfig = deriveRuntimeSmartRouterConfig(config, config);
|
|
14419
|
+
const text = input3.text.trim();
|
|
14420
|
+
const smartRouterEnabled = Boolean(smartRouterConfig?.enabled);
|
|
14421
|
+
const rules = smartRouterConfig?.rules ?? [];
|
|
14422
|
+
if (smartRouterEnabled && smartRouterConfig && text) {
|
|
14423
|
+
const ruleMatch = extractRuleMatch(text, rules);
|
|
14424
|
+
if (ruleMatch) {
|
|
14425
|
+
const model = resolveDisplayModel(config, ruleMatch.rule.model);
|
|
14426
|
+
return {
|
|
14427
|
+
input: input3,
|
|
14428
|
+
finalModel: model.resolved,
|
|
14429
|
+
finalModelRef: model.model,
|
|
14430
|
+
source: "smart_rule",
|
|
14431
|
+
confidence: 1,
|
|
14432
|
+
ruleName: ruleMatch.rule.name,
|
|
14433
|
+
steps: [
|
|
14434
|
+
{
|
|
14435
|
+
label: "SmartRouter.rules",
|
|
14436
|
+
status: "matched",
|
|
14437
|
+
detail: `\u547D\u4E2D\u89C4\u5219 "${ruleMatch.rule.name}"\uFF08${ruleMatch.detail}\uFF09\uFF0C\u4F1A\u5728\u57FA\u7840 Router \u524D\u9009\u5B9A\u6A21\u578B\u3002`
|
|
14438
|
+
}
|
|
14439
|
+
],
|
|
14440
|
+
warnings: []
|
|
14441
|
+
};
|
|
14442
|
+
}
|
|
14443
|
+
const semanticCandidates = buildSemanticCandidates(rules, smartRouterConfig);
|
|
14444
|
+
if (smartRouterConfig.semantic?.enabled && smartRouterConfig.semantic.mode !== "classifier" && semanticCandidates.length) {
|
|
14445
|
+
const result = semanticRouter.analyzeCandidates(
|
|
14446
|
+
text,
|
|
14447
|
+
semanticCandidates.map((candidate) => ({
|
|
14448
|
+
intent: candidate.intent,
|
|
14449
|
+
prototype: candidate.prototype,
|
|
14450
|
+
threshold: candidate.threshold
|
|
14451
|
+
})),
|
|
14452
|
+
smartRouterConfig.semantic.threshold
|
|
14453
|
+
);
|
|
14454
|
+
if (result) {
|
|
14455
|
+
const candidate = semanticCandidates.find((item) => item.intent === result.intent);
|
|
14456
|
+
const model = resolveDisplayModel(config, candidate?.rule.model);
|
|
14457
|
+
return {
|
|
14458
|
+
input: input3,
|
|
14459
|
+
finalModel: model.resolved,
|
|
14460
|
+
finalModelRef: model.model,
|
|
14461
|
+
source: "semantic_match",
|
|
14462
|
+
confidence: result.confidence,
|
|
14463
|
+
ruleName: result.intent,
|
|
14464
|
+
steps: [
|
|
14465
|
+
{
|
|
14466
|
+
label: "SmartRouter.semantic",
|
|
14467
|
+
status: "matched",
|
|
14468
|
+
detail: `\u8BED\u4E49 prototype \u547D\u4E2D "${result.intent}"\uFF0Cconfidence=${result.confidence.toFixed(3)}\u3002`
|
|
14469
|
+
}
|
|
14470
|
+
],
|
|
14471
|
+
warnings: []
|
|
14472
|
+
};
|
|
14473
|
+
}
|
|
14474
|
+
}
|
|
14475
|
+
if (smartRouterConfig.router_model && (smartRouterConfig.candidates?.length ?? 0) >= 2) {
|
|
14476
|
+
const model = resolveDisplayModel(config, smartRouterConfig.router_model);
|
|
14477
|
+
return {
|
|
14478
|
+
input: input3,
|
|
14479
|
+
finalModel: void 0,
|
|
14480
|
+
finalModelRef: void 0,
|
|
14481
|
+
source: "smart_router_pending",
|
|
14482
|
+
steps: [
|
|
14483
|
+
{
|
|
14484
|
+
label: "SmartRouter.router_model",
|
|
14485
|
+
status: "info",
|
|
14486
|
+
detail: `\u672A\u547D\u4E2D\u786E\u5B9A\u6027\u89C4\u5219\uFF1B\u771F\u5B9E\u8BF7\u6C42\u4F1A\u5148\u8C03\u7528 router_model "${smartRouterConfig.router_model}"\uFF08${model.resolved ?? "\u672A\u89E3\u6790"}\uFF09\u5728 ${smartRouterConfig.candidates?.length ?? 0} \u4E2A\u5019\u9009\u4E2D\u9009\u62E9\uFF0C\u4F1A\u589E\u52A0\u9996\u5305\u524D\u7B49\u5F85\u3002`
|
|
14487
|
+
},
|
|
14488
|
+
{
|
|
14489
|
+
label: "Basic Router fallback",
|
|
14490
|
+
status: "info",
|
|
14491
|
+
detail: "\u5982\u679C SmartRouter \u8C03\u7528\u5931\u8D25\u6216\u8FD4\u56DE\u65E0\u6548\u6A21\u578B\uFF0C\u8BF7\u6C42\u4F1A\u7EE7\u7EED\u8FDB\u5165\u57FA\u7840 Router fallback\u3002"
|
|
14492
|
+
}
|
|
14493
|
+
],
|
|
14494
|
+
warnings: [
|
|
14495
|
+
"doctor route preview \u4E0D\u4F1A\u8C03\u7528 SmartRouter LLM\uFF1B\u8FD9\u91CC\u5C55\u793A\u7684\u662F\u5F85\u51B3\u7B56\u8DEF\u5F84\uFF0C\u4E0D\u4EE3\u8868\u6700\u7EC8\u5019\u9009\u9009\u62E9\u3002"
|
|
14496
|
+
]
|
|
14497
|
+
};
|
|
14498
|
+
}
|
|
14499
|
+
}
|
|
14500
|
+
const basic = previewBasicRouter(config, input3);
|
|
14501
|
+
if (smartRouterEnabled) {
|
|
14502
|
+
basic.steps.unshift({
|
|
14503
|
+
label: "SmartRouter",
|
|
14504
|
+
status: "skipped",
|
|
14505
|
+
detail: "SmartRouter \u5DF2\u542F\u7528\uFF0C\u4F46\u672C\u6B21\u672A\u547D\u4E2D\u89C4\u5219/\u8BED\u4E49\uFF0C\u4E14\u6CA1\u6709\u53EF\u7528 router_model+candidates \u515C\u5E95\u3002"
|
|
14506
|
+
});
|
|
14507
|
+
}
|
|
14508
|
+
return basic;
|
|
14509
|
+
}
|
|
14510
|
+
function formatRoutePreview(result) {
|
|
14511
|
+
const lines = [
|
|
14512
|
+
`\u8DEF\u7531\u9884\u6F14\uFF1A${result.input.text || "<empty>"}`,
|
|
14513
|
+
`\u9884\u8BA1\u6765\u6E90\uFF1A${result.source}${result.ruleName ? ` (${result.ruleName})` : ""}`,
|
|
14514
|
+
`\u9884\u8BA1\u6A21\u578B\uFF1A${result.finalModelRef ?? "-"}${result.finalModel ? ` -> ${result.finalModel}` : ""}`
|
|
14515
|
+
];
|
|
14516
|
+
if (result.confidence !== void 0) {
|
|
14517
|
+
lines.push(`\u7F6E\u4FE1\u5EA6\uFF1A${result.confidence.toFixed(3)}`);
|
|
14518
|
+
}
|
|
14519
|
+
for (const step of result.steps) {
|
|
14520
|
+
lines.push(`- [${step.status}] ${step.label}: ${step.detail}`);
|
|
14521
|
+
}
|
|
14522
|
+
for (const warning of result.warnings) {
|
|
14523
|
+
lines.push(`! ${warning}`);
|
|
14524
|
+
}
|
|
14525
|
+
return lines;
|
|
14526
|
+
}
|
|
14527
|
+
var init_route_preview = __esm({
|
|
14528
|
+
"src/router/route-preview.ts"() {
|
|
14529
|
+
"use strict";
|
|
14530
|
+
init_compile();
|
|
14531
|
+
init_matcher();
|
|
14532
|
+
init_config();
|
|
14533
|
+
init_semantic_router();
|
|
14534
|
+
}
|
|
14535
|
+
});
|
|
14536
|
+
|
|
14158
14537
|
// src/doctor/index.ts
|
|
14159
14538
|
function collectCompatibilityPreviewDiagnostics(model) {
|
|
14160
14539
|
const registry = buildModelRegistry({
|
|
@@ -14222,6 +14601,18 @@ function collectCompatibilityPreviewDiagnostics(model) {
|
|
|
14222
14601
|
function hasArg(flag) {
|
|
14223
14602
|
return process.argv.slice(2).includes(flag);
|
|
14224
14603
|
}
|
|
14604
|
+
function readArgValue(flag) {
|
|
14605
|
+
const args = process.argv.slice(2);
|
|
14606
|
+
const index = args.indexOf(flag);
|
|
14607
|
+
if (index < 0) {
|
|
14608
|
+
return void 0;
|
|
14609
|
+
}
|
|
14610
|
+
const value = args[index + 1];
|
|
14611
|
+
if (!value || value.startsWith("--")) {
|
|
14612
|
+
return void 0;
|
|
14613
|
+
}
|
|
14614
|
+
return value;
|
|
14615
|
+
}
|
|
14225
14616
|
function createConsoleIO2() {
|
|
14226
14617
|
if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
|
|
14227
14618
|
const scriptedInput = (0, import_fs10.readFileSync)(0, "utf-8");
|
|
@@ -14807,6 +15198,57 @@ function reportRouterSlotSummary(config, registry, deps) {
|
|
|
14807
15198
|
deps.io.info("\u957F\u4E0A\u4E0B\u6587\u63D0\u793A\uFF1ARouter.longContext \u7684 context_window_tokens \u4E0D\u9AD8\u4E8E Router.default\uFF1B\u8BF7\u786E\u8BA4\u5B83\u786E\u5B9E\u662F\u957F\u4E0A\u4E0B\u6587\u6A21\u578B\u3002");
|
|
14808
15199
|
}
|
|
14809
15200
|
}
|
|
15201
|
+
function buildDoctorRoutePreviewInputs() {
|
|
15202
|
+
const customText = readArgValue("--route-text");
|
|
15203
|
+
if (customText) {
|
|
15204
|
+
return [{
|
|
15205
|
+
text: customText,
|
|
15206
|
+
model: readArgValue("--route-model") ?? "claude-3-5-sonnet",
|
|
15207
|
+
thinking: hasArg("--route-thinking"),
|
|
15208
|
+
webSearch: hasArg("--route-web-search"),
|
|
15209
|
+
tokenCount: Number(readArgValue("--route-tokens")) || void 0
|
|
15210
|
+
}];
|
|
15211
|
+
}
|
|
15212
|
+
return [
|
|
15213
|
+
{
|
|
15214
|
+
text: "\u65E5\u5E38\u4EE3\u7801\u4FEE\u6539\u548C\u89E3\u91CA",
|
|
15215
|
+
model: "claude-3-5-sonnet",
|
|
15216
|
+
tokenCount: 1e3
|
|
15217
|
+
},
|
|
15218
|
+
{
|
|
15219
|
+
text: "\u8BF7\u6DF1\u5165\u5206\u6790\u8FD9\u4E2A\u590D\u6742\u8BBE\u8BA1\u95EE\u9898",
|
|
15220
|
+
model: "claude-3-5-sonnet",
|
|
15221
|
+
thinking: true,
|
|
15222
|
+
tokenCount: 1e3
|
|
15223
|
+
},
|
|
15224
|
+
{
|
|
15225
|
+
text: "\u957F\u6587\u6863\u5168\u6587\u603B\u7ED3",
|
|
15226
|
+
model: "claude-3-5-sonnet",
|
|
15227
|
+
tokenCount: 12e4
|
|
15228
|
+
},
|
|
15229
|
+
{
|
|
15230
|
+
text: "\u9700\u8981\u8054\u7F51\u641C\u7D22\u8D44\u6599",
|
|
15231
|
+
model: "claude-3-5-sonnet",
|
|
15232
|
+
webSearch: true,
|
|
15233
|
+
tokenCount: 1e3
|
|
15234
|
+
},
|
|
15235
|
+
{
|
|
15236
|
+
text: "\u540E\u53F0\u8F7B\u91CF\u4EFB\u52A1",
|
|
15237
|
+
model: "claude-3-5-haiku",
|
|
15238
|
+
tokenCount: 1e3
|
|
15239
|
+
}
|
|
15240
|
+
];
|
|
15241
|
+
}
|
|
15242
|
+
function reportRoutePreview(config, deps) {
|
|
15243
|
+
deps.io.info("\u8DEF\u7531\u9884\u6F14\uFF1A\u6839\u636E\u5F53\u524D\u914D\u7F6E\u9884\u4F30\u8BF7\u6C42\u4F1A\u547D\u4E2D\u54EA\u4E2A\u6A21\u578B\uFF1B\u4E0D\u4F1A\u8C03\u7528\u4E0A\u6E38\u6A21\u578B\u6216 SmartRouter LLM\u3002");
|
|
15244
|
+
for (const input3 of buildDoctorRoutePreviewInputs()) {
|
|
15245
|
+
const result = previewRoute(config, input3);
|
|
15246
|
+
for (const line of formatRoutePreview(result)) {
|
|
15247
|
+
deps.io.info(line);
|
|
15248
|
+
}
|
|
15249
|
+
}
|
|
15250
|
+
deps.io.info("\u8DEF\u7531\u9884\u6F14\u63D0\u793A\uFF1A\u771F\u5B9E\u8FD0\u884C\u4ECD\u4F1A\u5728\u6700\u7EC8\u5B9A\u6A21\u540E\u6267\u884C context window guard\u3001\u6A21\u578B\u6C60 fallback\u3001\u8FDC\u7A0B\u4E2D\u8F6C\u548C\u6D41\u5F0F\u6CBB\u7406\u3002");
|
|
15251
|
+
}
|
|
14810
15252
|
function createDefaultDeps2(io = createConsoleIO2()) {
|
|
14811
15253
|
return {
|
|
14812
15254
|
readLegacyConfig,
|
|
@@ -14867,6 +15309,9 @@ async function runDoctorCli(customDeps) {
|
|
|
14867
15309
|
await reportRuntimeServiceContext(normalized.config, deps);
|
|
14868
15310
|
const registry = buildModelRegistry(normalized.config);
|
|
14869
15311
|
reportRouterSlotSummary(normalized.config, registry, deps);
|
|
15312
|
+
if (hasArg("--route-preview")) {
|
|
15313
|
+
reportRoutePreview(normalized.config, deps);
|
|
15314
|
+
}
|
|
14870
15315
|
for (const model of normalized.config.Models ?? []) {
|
|
14871
15316
|
const compiledModel = registry.modelMap[model.id];
|
|
14872
15317
|
if (!compiledModel) {
|
|
@@ -14954,6 +15399,7 @@ var init_doctor = __esm({
|
|
|
14954
15399
|
init_service_health();
|
|
14955
15400
|
init_templates();
|
|
14956
15401
|
init_api_keys();
|
|
15402
|
+
init_route_preview();
|
|
14957
15403
|
ROUTER_SLOT_DIAGNOSTICS = [
|
|
14958
15404
|
{
|
|
14959
15405
|
key: "default",
|
|
@@ -15104,6 +15550,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
15104
15550
|
\u4F7F\u7528\u793A\u4F8B\uFF1A
|
|
15105
15551
|
ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
|
|
15106
15552
|
ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
|
|
15553
|
+
ctr doctor --route-preview --route-text "\u8BF7\u505A\u67B6\u6784\u8BBE\u8BA1" # \u9884\u6F14\u5F53\u524D\u8BF7\u6C42\u4F1A\u8D70\u54EA\u4E2A\u6A21\u578B
|
|
15107
15554
|
ctr eval --tasks # \u67E5\u770B\u56FA\u5B9A\u8BC4\u6D4B\u4EFB\u52A1\u3001prompt \u548C rubric
|
|
15108
15555
|
ctr eval --input results.json # \u7528\u56FA\u5B9A\u4EFB\u52A1\u96C6 rubric \u8BC4\u6D4B\u591A\u6A21\u578B\u8F93\u51FA\u7ED3\u679C
|
|
15109
15556
|
ctr eval --run --models "sonnet;haiku" # \u81EA\u52A8\u8C03\u7528 CTR /v1/messages \u540E\u8BC4\u6D4B
|