@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/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);
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
- 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 = {}) {
@@ -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