@peterwangze/claude-trigger-router 1.2.0 → 1.4.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 +62 -8
- package/config/trigger.routing.yaml +67 -0
- package/config/trigger.smart-router.yaml +213 -0
- package/dist/cli.js +900 -24
- package/dist/cli.js.map +3 -3
- package/docs/configuration-guide.md +4 -0
- package/docs/release-notes-v1.2.0.md +6 -3
- package/docs/release-notes-v1.3.0.md +39 -0
- package/docs/release-notes-v1.4.0.md +40 -0
- package/docs/releasing.md +3 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2462,6 +2462,7 @@ function createGovernanceTrace(input3 = {}) {
|
|
|
2462
2462
|
initialModel: input3.initialModel,
|
|
2463
2463
|
finalModel: input3.finalModel,
|
|
2464
2464
|
routeReason: input3.routeReason ? [...input3.routeReason] : [],
|
|
2465
|
+
routeDecision: input3.routeDecision ? { ...input3.routeDecision } : void 0,
|
|
2465
2466
|
stickyHit: input3.stickyHit ?? false,
|
|
2466
2467
|
alignmentUsed: input3.alignmentUsed ?? false,
|
|
2467
2468
|
semanticIntent: input3.semanticIntent,
|
|
@@ -2496,6 +2497,192 @@ function recordGovernanceTrace(trace) {
|
|
|
2496
2497
|
governanceTraceStore.add(trace);
|
|
2497
2498
|
return trace;
|
|
2498
2499
|
}
|
|
2500
|
+
function formatPercent(value) {
|
|
2501
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2502
|
+
return void 0;
|
|
2503
|
+
}
|
|
2504
|
+
return `${Math.round(value * 100)}%`;
|
|
2505
|
+
}
|
|
2506
|
+
function firstReason(trace, prefix) {
|
|
2507
|
+
return trace.routeReason.find((reason) => reason === prefix || reason.startsWith(`${prefix}:`));
|
|
2508
|
+
}
|
|
2509
|
+
function inferRouteSource(trace) {
|
|
2510
|
+
if (trace.routeDecision?.source) {
|
|
2511
|
+
return trace.routeDecision.source;
|
|
2512
|
+
}
|
|
2513
|
+
if (firstReason(trace, "smart_rule")) {
|
|
2514
|
+
return "smart_rule";
|
|
2515
|
+
}
|
|
2516
|
+
if (firstReason(trace, "semantic_match") || firstReason(trace, "semantic:intent")) {
|
|
2517
|
+
return "semantic_match";
|
|
2518
|
+
}
|
|
2519
|
+
if (trace.stickyHit || firstReason(trace, "sticky_correction") || firstReason(trace, "sticky")) {
|
|
2520
|
+
return "sticky_correction";
|
|
2521
|
+
}
|
|
2522
|
+
if (firstReason(trace, "smart_router")) {
|
|
2523
|
+
return "smart_router";
|
|
2524
|
+
}
|
|
2525
|
+
if (firstReason(trace, "context_window_fallback") || firstReason(trace, "context_window_exceeded")) {
|
|
2526
|
+
return "context_window_guard";
|
|
2527
|
+
}
|
|
2528
|
+
if (firstReason(trace, "model_pool_fallback") || trace.modelPoolFallbackTriggered) {
|
|
2529
|
+
return "model_pool_fallback";
|
|
2530
|
+
}
|
|
2531
|
+
if (trace.cascadeTriggered || firstReason(trace, "cascade_gate")) {
|
|
2532
|
+
return "cascade";
|
|
2533
|
+
}
|
|
2534
|
+
return "basic_router";
|
|
2535
|
+
}
|
|
2536
|
+
function inferRuleName(trace) {
|
|
2537
|
+
if (trace.routeDecision?.ruleName) {
|
|
2538
|
+
return trace.routeDecision.ruleName;
|
|
2539
|
+
}
|
|
2540
|
+
const ruleReason = firstReason(trace, "smart_rule") ?? firstReason(trace, "semantic_match");
|
|
2541
|
+
return ruleReason?.split(":").slice(1).join(":") || void 0;
|
|
2542
|
+
}
|
|
2543
|
+
function inferFallbackReason(trace) {
|
|
2544
|
+
if (trace.routeDecision?.fallbackReason) {
|
|
2545
|
+
return trace.routeDecision.fallbackReason;
|
|
2546
|
+
}
|
|
2547
|
+
const contextFallback = firstReason(trace, "context_window_fallback");
|
|
2548
|
+
if (contextFallback) {
|
|
2549
|
+
const transition = contextFallback.split(":").slice(1).join(":");
|
|
2550
|
+
return transition ? `Context window guard switched ${transition}.` : "Context window guard switched to the long-context route.";
|
|
2551
|
+
}
|
|
2552
|
+
const contextExceeded = firstReason(trace, "context_window_exceeded");
|
|
2553
|
+
if (contextExceeded) {
|
|
2554
|
+
const model = contextExceeded.split(":").slice(1).join(":");
|
|
2555
|
+
return model ? `Selected model "${model}" exceeded context limits and no long-context fallback fit.` : "Selected model exceeded context limits and no long-context fallback fit.";
|
|
2556
|
+
}
|
|
2557
|
+
if (trace.modelPoolFallbackTriggered) {
|
|
2558
|
+
const from = trace.modelPoolFallbackFromEndpoint ?? "current endpoint";
|
|
2559
|
+
const to = trace.modelPoolFallbackNextEndpoint ?? "next endpoint";
|
|
2560
|
+
return `Model pool fallback moved from ${from} to ${to}${trace.modelPoolFallbackEvidence ? ` (${trace.modelPoolFallbackEvidence})` : ""}.`;
|
|
2561
|
+
}
|
|
2562
|
+
const poolFallback = firstReason(trace, "model_pool_fallback");
|
|
2563
|
+
if (poolFallback) {
|
|
2564
|
+
const [, modelId, endpointId] = poolFallback.split(":");
|
|
2565
|
+
return modelId && endpointId ? `Model pool fallback tried endpoint "${endpointId}" for "${modelId}".` : "Model pool fallback was attempted.";
|
|
2566
|
+
}
|
|
2567
|
+
if (trace.cascadeTriggered) {
|
|
2568
|
+
const evidence = trace.cascadeEvidence?.length ? `: ${trace.cascadeEvidence.join(", ")}` : "";
|
|
2569
|
+
return `Cascade retry was triggered${evidence}.`;
|
|
2570
|
+
}
|
|
2571
|
+
if (trace.routeReason.includes("smart_router:no_match")) {
|
|
2572
|
+
return "SmartRouter did not match; request continued to the basic Router fallback path.";
|
|
2573
|
+
}
|
|
2574
|
+
return void 0;
|
|
2575
|
+
}
|
|
2576
|
+
function summarizeRouteDecisionTrace(trace) {
|
|
2577
|
+
const source = inferRouteSource(trace);
|
|
2578
|
+
const ruleName = inferRuleName(trace);
|
|
2579
|
+
const confidence = trace.routeDecision?.confidence;
|
|
2580
|
+
const confidenceLabel = formatPercent(confidence);
|
|
2581
|
+
const finalModel = trace.finalModel ?? trace.routeDecision?.model;
|
|
2582
|
+
const fallbackReason = inferFallbackReason(trace);
|
|
2583
|
+
const sourceLabels = {
|
|
2584
|
+
smart_rule: ruleName ? `SmartRouter rule "${ruleName}"` : "SmartRouter rule",
|
|
2585
|
+
semantic_match: ruleName ? `Semantic match "${ruleName}"` : "Semantic match",
|
|
2586
|
+
smart_router: "SmartRouter candidate selection",
|
|
2587
|
+
no_match: "SmartRouter no match",
|
|
2588
|
+
sticky_correction: "Sticky routing",
|
|
2589
|
+
context_window_guard: "Context window guard",
|
|
2590
|
+
model_pool_fallback: "Model pool fallback",
|
|
2591
|
+
cascade: "Cascade retry",
|
|
2592
|
+
basic_router: "Basic Router"
|
|
2593
|
+
};
|
|
2594
|
+
const sourceLabel = sourceLabels[source] ?? source;
|
|
2595
|
+
const selectedText = finalModel ? ` selected ${finalModel}` : " handled the request";
|
|
2596
|
+
const confidenceText = confidenceLabel ? ` with ${confidenceLabel} confidence` : "";
|
|
2597
|
+
const headline = `${sourceLabel}${selectedText}${confidenceText}.`;
|
|
2598
|
+
return {
|
|
2599
|
+
requestId: trace.requestId,
|
|
2600
|
+
sessionKey: trace.sessionKey,
|
|
2601
|
+
source,
|
|
2602
|
+
sourceLabel,
|
|
2603
|
+
ruleName,
|
|
2604
|
+
semanticIntent: trace.semanticIntent,
|
|
2605
|
+
confidence,
|
|
2606
|
+
confidenceLabel,
|
|
2607
|
+
initialModel: trace.initialModel,
|
|
2608
|
+
finalModel,
|
|
2609
|
+
fallbackReason,
|
|
2610
|
+
routeReasons: [...trace.routeReason],
|
|
2611
|
+
headline,
|
|
2612
|
+
continuity: {
|
|
2613
|
+
stickyHit: trace.stickyHit,
|
|
2614
|
+
alignmentUsed: trace.alignmentUsed,
|
|
2615
|
+
cascadeTriggered: trace.cascadeTriggered,
|
|
2616
|
+
shadowChecked: trace.shadowChecked
|
|
2617
|
+
},
|
|
2618
|
+
latencyMs: trace.latencyMs,
|
|
2619
|
+
startedAt: trace.startedAt,
|
|
2620
|
+
completedAt: trace.completedAt
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
function summarizeSwitchContinuityTrace(trace) {
|
|
2624
|
+
const decision = summarizeRouteDecisionTrace(trace);
|
|
2625
|
+
const initialModel = trace.initialModel;
|
|
2626
|
+
const finalModel = trace.finalModel ?? trace.routeDecision?.model;
|
|
2627
|
+
const switched = Boolean(initialModel && finalModel && initialModel !== finalModel);
|
|
2628
|
+
const transition = initialModel && finalModel ? `${initialModel} -> ${finalModel}` : void 0;
|
|
2629
|
+
const hasModelPair = Boolean(initialModel && finalModel);
|
|
2630
|
+
let status = "unknown";
|
|
2631
|
+
let headline = "Model continuity is not available for this request.";
|
|
2632
|
+
let action = "Record both initialModel and finalModel so users can tell whether model switching affected the response.";
|
|
2633
|
+
if (hasModelPair && !switched) {
|
|
2634
|
+
status = "stable";
|
|
2635
|
+
headline = trace.stickyHit ? `Sticky routing kept the request on ${finalModel}.` : `Model stayed on ${finalModel}; no continuity handoff was needed.`;
|
|
2636
|
+
action = "No action needed unless this route should intentionally explore stronger or faster candidates.";
|
|
2637
|
+
} else if (switched && trace.alignmentUsed && !trace.cascadeTriggered) {
|
|
2638
|
+
status = "aligned";
|
|
2639
|
+
headline = `Model switched ${transition} with context alignment.`;
|
|
2640
|
+
action = "Keep this as positive switching evidence; compare latency and output quality before widening the route.";
|
|
2641
|
+
} else if (switched && trace.alignmentUsed && trace.cascadeTriggered) {
|
|
2642
|
+
status = "watch";
|
|
2643
|
+
headline = `Model switched ${transition} with alignment, but cascade retry still triggered.`;
|
|
2644
|
+
action = "Review cascade evidence and consider narrowing this route or moving the task to the retry model directly.";
|
|
2645
|
+
} else if (switched && !trace.alignmentUsed && trace.cascadeTriggered) {
|
|
2646
|
+
status = "critical";
|
|
2647
|
+
headline = `Model switched ${transition} without alignment and then triggered cascade retry.`;
|
|
2648
|
+
action = "Enable or tune Governance.sticky.alignment before sending more traffic through this switching path.";
|
|
2649
|
+
} else if (switched) {
|
|
2650
|
+
status = "watch";
|
|
2651
|
+
headline = `Model switched ${transition} without context alignment.`;
|
|
2652
|
+
action = "Enable or tune Governance.sticky.alignment if this route can carry multi-turn or long-running work.";
|
|
2653
|
+
}
|
|
2654
|
+
const fallbackReason = decision.fallbackReason;
|
|
2655
|
+
const detail = [
|
|
2656
|
+
decision.sourceLabel,
|
|
2657
|
+
decision.ruleName ? `rule ${decision.ruleName}` : void 0,
|
|
2658
|
+
trace.semanticIntent ? `intent ${trace.semanticIntent}` : void 0,
|
|
2659
|
+
fallbackReason
|
|
2660
|
+
].filter((item) => Boolean(item));
|
|
2661
|
+
return {
|
|
2662
|
+
requestId: trace.requestId,
|
|
2663
|
+
sessionKey: trace.sessionKey,
|
|
2664
|
+
status,
|
|
2665
|
+
switched,
|
|
2666
|
+
initialModel,
|
|
2667
|
+
finalModel,
|
|
2668
|
+
transition,
|
|
2669
|
+
source: decision.source,
|
|
2670
|
+
sourceLabel: decision.sourceLabel,
|
|
2671
|
+
ruleName: decision.ruleName,
|
|
2672
|
+
semanticIntent: trace.semanticIntent,
|
|
2673
|
+
stickyHit: trace.stickyHit,
|
|
2674
|
+
alignmentUsed: trace.alignmentUsed,
|
|
2675
|
+
cascadeTriggered: trace.cascadeTriggered,
|
|
2676
|
+
cascadeEvidence: trace.cascadeEvidence ? [...trace.cascadeEvidence] : void 0,
|
|
2677
|
+
fallbackReason,
|
|
2678
|
+
detail,
|
|
2679
|
+
headline,
|
|
2680
|
+
action,
|
|
2681
|
+
latencyMs: trace.latencyMs,
|
|
2682
|
+
startedAt: trace.startedAt,
|
|
2683
|
+
completedAt: trace.completedAt
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2499
2686
|
var import_crypto2, import_fs2, import_promises2, import_lru_cache, import_path3, import_zlib, GovernanceTraceStore, governanceTraceStore;
|
|
2500
2687
|
var init_trace = __esm({
|
|
2501
2688
|
"src/governance/trace.ts"() {
|
|
@@ -2535,7 +2722,11 @@ var init_trace = __esm({
|
|
|
2535
2722
|
this.loadFromDisk();
|
|
2536
2723
|
}
|
|
2537
2724
|
add(trace) {
|
|
2538
|
-
this.cache.set(trace.requestId, {
|
|
2725
|
+
this.cache.set(trace.requestId, {
|
|
2726
|
+
...trace,
|
|
2727
|
+
routeReason: [...trace.routeReason],
|
|
2728
|
+
routeDecision: trace.routeDecision ? { ...trace.routeDecision } : void 0
|
|
2729
|
+
});
|
|
2539
2730
|
this.schedulePersistToDisk();
|
|
2540
2731
|
}
|
|
2541
2732
|
get(requestId) {
|
|
@@ -4500,6 +4691,27 @@ function topSlowOutcomeGroup(groups) {
|
|
|
4500
4691
|
return left.key.localeCompare(right.key);
|
|
4501
4692
|
})[0];
|
|
4502
4693
|
}
|
|
4694
|
+
function smartRouterTargetPath(routeKey) {
|
|
4695
|
+
if (!routeKey) {
|
|
4696
|
+
return "SmartRouter";
|
|
4697
|
+
}
|
|
4698
|
+
if (routeKey.startsWith("smart_rule:")) {
|
|
4699
|
+
const ruleName = routeKey.slice("smart_rule:".length);
|
|
4700
|
+
return ruleName ? `SmartRouter.rules[name="${ruleName}"]` : "SmartRouter.rules";
|
|
4701
|
+
}
|
|
4702
|
+
if (routeKey.startsWith("semantic_match:")) {
|
|
4703
|
+
const intent = routeKey.slice("semantic_match:".length);
|
|
4704
|
+
return intent ? `SmartRouter.semantic.prototypes.${intent}` : "SmartRouter.semantic";
|
|
4705
|
+
}
|
|
4706
|
+
if (routeKey.startsWith("semantic:intent:")) {
|
|
4707
|
+
const intent = routeKey.slice("semantic:intent:".length);
|
|
4708
|
+
return intent ? `SmartRouter.semantic.prototypes.${intent}` : "SmartRouter.semantic";
|
|
4709
|
+
}
|
|
4710
|
+
if (routeKey === "smart_router") {
|
|
4711
|
+
return "SmartRouter.candidates";
|
|
4712
|
+
}
|
|
4713
|
+
return "SmartRouter.rules";
|
|
4714
|
+
}
|
|
4503
4715
|
function buildRoutingTuningRecommendations(metrics, outcome) {
|
|
4504
4716
|
if (!outcome || metrics.totalTraces === 0) {
|
|
4505
4717
|
return [];
|
|
@@ -4520,7 +4732,17 @@ function buildRoutingTuningRecommendations(metrics, outcome) {
|
|
|
4520
4732
|
severity: "critical",
|
|
4521
4733
|
message: "Some requests exceeded the selected model context window.",
|
|
4522
4734
|
evidence: `contextWindowExceededRate=${percent(outcome.contextWindowExceededRate)}`,
|
|
4523
|
-
action: "Review model context window metadata and Router.longContext coverage."
|
|
4735
|
+
action: "Review model context window metadata and Router.longContext coverage.",
|
|
4736
|
+
configSuggestions: [
|
|
4737
|
+
{
|
|
4738
|
+
path: "Models[].metadata.context_window_tokens",
|
|
4739
|
+
reason: "Fill context window metadata so CTR can detect oversize requests before sending them upstream."
|
|
4740
|
+
},
|
|
4741
|
+
{
|
|
4742
|
+
path: "Router.longContext",
|
|
4743
|
+
reason: "Point long-context traffic to the largest safe model instead of letting small models receive oversized prompts."
|
|
4744
|
+
}
|
|
4745
|
+
]
|
|
4524
4746
|
});
|
|
4525
4747
|
} else if (outcome.contextWindowFallbackRate >= 0.3) {
|
|
4526
4748
|
recommendations.push({
|
|
@@ -4528,7 +4750,17 @@ function buildRoutingTuningRecommendations(metrics, outcome) {
|
|
|
4528
4750
|
severity: outcome.contextWindowFallbackRate >= 0.6 ? "warn" : "info",
|
|
4529
4751
|
message: "Long-context fallback is frequent enough to affect latency planning.",
|
|
4530
4752
|
evidence: `contextWindowFallbackRate=${percent(outcome.contextWindowFallbackRate)}`,
|
|
4531
|
-
action: "Monitor context window fallback rate and long-context model latency."
|
|
4753
|
+
action: "Monitor context window fallback rate and long-context model latency.",
|
|
4754
|
+
configSuggestions: [
|
|
4755
|
+
{
|
|
4756
|
+
path: "SmartRouter.rules",
|
|
4757
|
+
reason: "Add or raise an explicit long-context rule when the same task class repeatedly falls back after initial selection."
|
|
4758
|
+
},
|
|
4759
|
+
{
|
|
4760
|
+
path: "Router.longContext",
|
|
4761
|
+
reason: "Keep the long-context route on a model with enough context and acceptable latency."
|
|
4762
|
+
}
|
|
4763
|
+
]
|
|
4532
4764
|
});
|
|
4533
4765
|
}
|
|
4534
4766
|
const switchWithoutAlignment = topOutcomeGroup(
|
|
@@ -4541,7 +4773,19 @@ function buildRoutingTuningRecommendations(metrics, outcome) {
|
|
|
4541
4773
|
severity: "warn",
|
|
4542
4774
|
message: "A high-switch route is not consistently using alignment.",
|
|
4543
4775
|
evidence: `${switchWithoutAlignment.key}:switch=${percent(switchWithoutAlignment.modelSwitchRate)}:alignment=${percent(switchWithoutAlignment.alignmentOnSwitchRate)}`,
|
|
4544
|
-
action: "Enable or tune SmartRouter sticky alignment for high-switch routes."
|
|
4776
|
+
action: "Enable or tune SmartRouter sticky alignment for high-switch routes.",
|
|
4777
|
+
configSuggestions: [
|
|
4778
|
+
{
|
|
4779
|
+
path: "SmartRouter.sticky.enabled",
|
|
4780
|
+
suggestedValue: true,
|
|
4781
|
+
reason: "Keep related requests on a stable model unless an explicit route needs to break stickiness."
|
|
4782
|
+
},
|
|
4783
|
+
{
|
|
4784
|
+
path: "SmartRouter.sticky.alignment.enabled",
|
|
4785
|
+
suggestedValue: true,
|
|
4786
|
+
reason: "Inject a compact handoff summary when a model switch is unavoidable."
|
|
4787
|
+
}
|
|
4788
|
+
]
|
|
4545
4789
|
});
|
|
4546
4790
|
}
|
|
4547
4791
|
const cascadeAfterSwitch = topOutcomeGroup(
|
|
@@ -4556,7 +4800,17 @@ function buildRoutingTuningRecommendations(metrics, outcome) {
|
|
|
4556
4800
|
severity,
|
|
4557
4801
|
message: "Model switches are followed by cascade retries often enough to review policy.",
|
|
4558
4802
|
evidence: cascadeAfterSwitch ? `${cascadeAfterSwitch.key}:cascadeAfterSwitch=${percent(cascadeAfterSwitch.cascadeAfterSwitchRate)}` : `cascadeAfterSwitchRate=${percent(outcome.cascadeAfterSwitchRate)}`,
|
|
4559
|
-
action: "Review high-cascade route groups before widening SmartRouter candidates."
|
|
4803
|
+
action: "Review high-cascade route groups before widening SmartRouter candidates.",
|
|
4804
|
+
configSuggestions: [
|
|
4805
|
+
{
|
|
4806
|
+
path: smartRouterTargetPath(cascadeAfterSwitch?.key),
|
|
4807
|
+
reason: "Narrow this route or move its model directly to the retry target when cascade evidence repeatedly follows selection."
|
|
4808
|
+
},
|
|
4809
|
+
{
|
|
4810
|
+
path: "SmartRouter.candidates",
|
|
4811
|
+
reason: "Remove or demote candidates that often need cascade retry for this route class."
|
|
4812
|
+
}
|
|
4813
|
+
]
|
|
4560
4814
|
});
|
|
4561
4815
|
}
|
|
4562
4816
|
const slowRoute = topSlowOutcomeGroup(outcome.byRouteReason);
|
|
@@ -4566,7 +4820,17 @@ function buildRoutingTuningRecommendations(metrics, outcome) {
|
|
|
4566
4820
|
severity: slowRoute.averageLatencyMs >= DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs ? "critical" : "warn",
|
|
4567
4821
|
message: "A route group is slower than the governance latency warning threshold.",
|
|
4568
4822
|
evidence: `${slowRoute.key}:averageLatencyMs=${slowRoute.averageLatencyMs}`,
|
|
4569
|
-
action: "Inspect slow route groups before making them default traffic."
|
|
4823
|
+
action: "Inspect slow route groups before making them default traffic.",
|
|
4824
|
+
configSuggestions: [
|
|
4825
|
+
{
|
|
4826
|
+
path: smartRouterTargetPath(slowRoute.key),
|
|
4827
|
+
reason: "Route this slow task class to a faster model, lower the rule priority, or split the rule into fast and deep variants."
|
|
4828
|
+
},
|
|
4829
|
+
{
|
|
4830
|
+
path: "SmartRouter.candidates",
|
|
4831
|
+
reason: "Prefer candidates with proven lower latency for frequent tasks, and reserve slower models for explicit deep-work rules."
|
|
4832
|
+
}
|
|
4833
|
+
]
|
|
4570
4834
|
});
|
|
4571
4835
|
}
|
|
4572
4836
|
return recommendations.slice(0, 5);
|
|
@@ -5352,7 +5616,7 @@ function renderWorkbenchHtml(rawInitialConfig, configuredThresholds = {}) {
|
|
|
5352
5616
|
const escapedCascadeWarnRate = escapeHtml(configuredThresholds.cascade_warn_rate ?? 0.4);
|
|
5353
5617
|
const escapedShadowWarnRate = escapeHtml(configuredThresholds.shadow_warn_rate ?? 0.5);
|
|
5354
5618
|
const escapedLatencyWarnMs = escapeHtml(configuredThresholds.latency_warn_ms ?? 1500);
|
|
5355
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:.75rem 1rem;flex-wrap:wrap;align-items:flex-start;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.scope-guide{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:.75rem;margin-top:.75rem}.scope-guide div{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem}.scope-guide strong{display:block;margin-bottom:.35rem}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}.pill.info{background:#eff6ff;color:#1d4ed8}.pill.warn{background:#fff7ed;color:#9a3412}.pill.critical{background:#fef2f2;color:#991b1b}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Mode</span><strong id="serviceModeStatus">${escapedRuntimeMode}</strong></div><div class="status-tile"><span class="muted">Role</span><strong id="serviceRoleStatus">${escapedServiceRole}</strong></div><div class="status-tile"><span class="muted">Listener</span><strong id="listenerStatusSummary">${escapedListenerSummary}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div><div class="status-tile"><span class="muted">Remote service</span><strong id="remoteStatusSummary">${escapedRemoteSummary}</strong></div><div class="status-tile"><span class="muted">Remote registration</span><strong id="remoteRegistrationStatusSummary">checking</strong></div><div class="status-tile"><span class="muted">Registration</span><strong id="registrationStatusSummary">${escapedRegistrationSummary}</strong></div><div class="status-tile"><span class="muted">Auth</span><strong id="authStatusSummary">${escapedAuthSummary}</strong></div><div class="status-tile"><span class="muted">Security</span><strong id="securityStatusSummary">${escapedSecuritySummary}</strong></div></div></div></div><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div class="alert info"><strong>Models field guide</strong><div class="muted">\u65B0\u914D\u7F6E\u8BF7\u4F7F\u7528\u5165\u53E3\u5B57\u6BB5\uFF1Aid / api / key / interface / model / thinking / metadata\uFF1Bapi_key / api_base_url / protocol \u4EC5\u4F5C\u4E3A\u65E7\u914D\u7F6E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" aria-label="JSON config draft" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4","thinking":"auto","metadata":{"vendor_hint":"openrouter"}}],"Router":{"default":"sonnet"}}'></textarea><div class="muted">JSON \u8349\u7A3F\u540C\u6837\u5EFA\u8BAE\u53EA\u5199\u5165\u53E3\u5B57\u6BB5\uFF1B\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u5F52\u4E00\uFF0C\u65E7\u5B57\u6BB5\u522B\u540D\u65E0\u9700\u624B\u52A8\u8865\u5145\u3002</div><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model pools</strong><span class="muted">Registration.models \u7F16\u8BD1\u51FA\u7684\u540C\u6A21\u578B\u591A\u6E90\u6C60\uFF0C\u5F53\u524D\u652F\u6301 priority / least-latency active endpoint\u3001\u975E\u6D41\u5F0F\u9519\u8BEF fallback\u3001\u5185\u5B58 health/cooldown\u3001\u7194\u65AD\u72B6\u6001\u4E0E\u5EF6\u8FDF\u7A97\u53E3</span></div><table id="compiledModelPoolsTable" class="management-table"><thead><tr><th>Pool</th><th>Strategy</th><th>Active endpoint</th><th>Endpoints</th><th>Warnings</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading model pools...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div id="securitySummary" class="alert info"><strong>Security pending</strong><div class="muted">\u7B49\u5F85\u670D\u52A1\u5B89\u5168\u72B6\u6001\u52A0\u8F7D</div></div><div class="subpanel" id="roleConnectionGuide"><div class="row"><strong>Role & connection guide</strong><span class="muted">\u6309\u5F53\u524D local / server / cloud \u89D2\u8272\u786E\u8BA4\u76D1\u542C\u5730\u5740\u3001\u7EF4\u62A4\u5165\u53E3\u548C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\u65B9\u5F0F\u3002</span></div><div class="scope-guide"><div><strong>current role</strong><span id="roleConnectionSummary" class="muted">${escapedRuntimeMode} / ${escapedServiceRole}</span></div><div><strong>listener</strong><span id="listenerConnectionSummary" class="muted">${escapedListenerSummary}</span></div><div><strong>remote clients</strong><span id="clientConnectionSummary" class="muted">${escapedClientConnectionSummary}</span></div></div><div class="muted" style="margin-top:.75rem">${escapedLocalUserRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedServerMaintainerRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedRemoteClientRoleGuide}</div></div><div class="subpanel" id="authScopeGuide"><div class="row"><strong>Auth scope guide</strong><span class="muted">\u6309\u7528\u9014\u53D1\u653E\u6700\u5C0F\u6743\u9650 key\uFF0C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u4E0D\u8981\u590D\u7528 admin key\u3002</span></div><div class="scope-guide"><div><strong>admin</strong><span class="muted">\u670D\u52A1\u6240\u6709\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001auth \u7BA1\u7406\uFF0C\u4EE5\u53CA\u6240\u6709\u8FD0\u7EF4\u5199\u64CD\u4F5C\u3002</span></div><div><strong>operator</strong><span class="muted">\u65E5\u5E38\u8FD0\u7EF4\u4F7F\u7528\uFF1A\u91CD\u542F\u3001\u6CBB\u7406\u5FEB\u7167\u3001\u5B9A\u65F6\u5FEB\u7167\u3001\u5F02\u5E38\u9608\u503C\u548C\u5F52\u6863\u5220\u9664\uFF1B\u4E0D\u80FD\u67E5\u770B\u914D\u7F6E\u6216\u7BA1\u7406 auth\u3002</span></div><div><strong>client</strong><span class="muted">\u5BA2\u6237\u7AEF\u6A21\u578B\u8C03\u7528\uFF1A/v1/messages\u3001/v1/chat/completions\uFF1B\u6A21\u578B\u8C03\u7528\u914D\u989D\u53EA\u8BA1\u5165\u8FD9\u91CC\u3002</span></div><div><strong>read-only</strong><span class="muted">\u53EA\u8BFB\u89C2\u6D4B\uFF1Ahealth\u3001service-info\u3001compiled models\u3001model pool health\u3001transformers \u548C governance GET\u3002</span></div><div><strong>client + read-only</strong><span class="muted">\u8FDC\u7A0B token \u540C\u65F6\u9700\u8981 ready/status \u63A2\u6D4B\u4E0E\u6A21\u578B\u8C03\u7528\u65F6\u4F7F\u7528\u8BE5\u7EC4\u5408\u3002</span></div></div><div class="muted" style="margin-top:.75rem">\u7BA1\u7406\u5165\u53E3\uFF1A\u7528 admin key \u8C03\u7528 <code>GET /api/auth/keys</code> \u67E5\u770B\u5217\u8868\uFF0C<code>POST /api/auth/keys</code> \u751F\u6210 key\uFF0C<code>POST /api/auth/keys/:id/revoke</code> \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\uFF0C\u8BF7\u76F4\u63A5\u4EA4\u7ED9\u5BF9\u5E94\u5BA2\u6237\u7AEF\u4FDD\u5B58\u3002</div></div><div class="subpanel"><div class="row"><strong>Auth quota</strong><span class="muted">\u6309 managed key \u67E5\u770B\u6A21\u578B\u8C03\u7528\u914D\u989D\u3001\u5F53\u524D\u7528\u91CF\u4E0E\u7A97\u53E3\u91CD\u7F6E\u65F6\u95F4</span></div><table id="authQuotaTable" class="management-table"><thead><tr><th>Key</th><th>Scope</th><th>Status</th><th>Requests</th><th>Tokens</th><th>Window</th></tr></thead><tbody><tr><td colspan="6" class="muted">Waiting for service status...</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Model pool health</strong><span class="muted">\u67E5\u770B\u540C\u6A21\u578B\u591A\u6E90\u6C60\u7684 active endpoint\u3001\u6301\u4E45\u5316\u72B6\u6001\u3001cooldown\u3001\u7194\u65AD\u4E0E\u5EF6\u8FDF\u7A97\u53E3\u3002</span></div><div id="modelPoolHealthSummary" class="alert info"><strong>Pool health pending</strong><div class="muted">\u7B49\u5F85\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001\u52A0\u8F7D</div></div><table id="modelPoolHealthTable" class="management-table"><thead><tr><th>Pool</th><th>Endpoint</th><th>Status</th><th>Latency</th><th>Failures</th><th>Last success</th><th>Recovery</th></tr></thead><tbody><tr><td colspan="7" class="muted">Waiting for model pool health...</td></tr></tbody></table></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/pool-health</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Health</span><strong>-</strong></div><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Model switch rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment on switch</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="healthSummary" class="alert info"><strong>Health pending</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u5065\u5EB7\u6458\u8981\u52A0\u8F7D</div></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Routing tuning</strong><span class="muted">\u57FA\u4E8E outcome \u8BC1\u636E\u7ED9\u51FA SmartRouter \u8C03\u4F18\u5EFA\u8BAE</span></div><ul id="routingTuningList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Quality evidence</strong><span class="muted">\u771F\u5B9E trace \u4E2D\u7684\u5931\u8D25\u3001\u8FDE\u7EED\u6027\u548C\u901F\u5EA6\u98CE\u9669\u6837\u672C</span></div><div id="qualityEvidenceSummary" class="stats"><div class="stat"><span class="muted">Samples</span><strong>-</strong></div><div class="stat"><span class="muted">Risk</span><strong>-</strong></div><div class="stat"><span class="muted">Improvement</span><strong>-</strong></div><div class="stat"><span class="muted">Speed risk</span><strong>-</strong></div></div><ul id="qualityEvidenceList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Task comparison</strong><span class="muted">\u540C\u7C7B\u4EFB\u52A1\u4E0B\u4E0D\u540C\u6700\u7EC8\u6A21\u578B\u7684\u5931\u8D25\u7387\u548C\u901F\u5EA6\u5BF9\u6BD4</span></div><div id="taskComparisonSummary" class="stats"><div class="stat"><span class="muted">Tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Traces</span><strong>-</strong></div></div><ul id="taskComparisonList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by route</strong><span class="muted">\u5207\u6362\u3001alignment\u3001cascade \u4E0E\u5EF6\u8FDF</span></div><ul id="routeOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by model</strong><span class="muted">\u6700\u7EC8\u6A21\u578B\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="modelOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by intent</strong><span class="muted">\u4EFB\u52A1\u610F\u56FE\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="intentOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\u6458\u8981</li><li><code>GET /api/auth/audit</code> \u2014 \u67E5\u770B\u9274\u6743\u5BA1\u8BA1\u6458\u8981</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const listenerStatusSummary=document.getElementById('listenerStatusSummary');const roleConnectionSummary=document.getElementById('roleConnectionSummary');const listenerConnectionSummary=document.getElementById('listenerConnectionSummary');const clientConnectionSummary=document.getElementById('clientConnectionSummary');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const authStatusSummary=document.getElementById('authStatusSummary');const securityStatusSummary=document.getElementById('securityStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const compiledModelPoolsTableBody=document.querySelector('#compiledModelPoolsTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const routeOutcomeRanking=document.getElementById('routeOutcomeRanking');const modelOutcomeRanking=document.getElementById('modelOutcomeRanking');const intentOutcomeRanking=document.getElementById('intentOutcomeRanking');const healthSummary=document.getElementById('healthSummary');const routingTuningList=document.getElementById('routingTuningList');const qualityEvidenceSummary=document.getElementById('qualityEvidenceSummary');const qualityEvidenceList=document.getElementById('qualityEvidenceList');const taskComparisonSummary=document.getElementById('taskComparisonSummary');const taskComparisonList=document.getElementById('taskComparisonList');const securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable tbody');const modelPoolHealthSummary=document.getElementById('modelPoolHealthSummary');const modelPoolHealthTableBody=document.querySelector('#modelPoolHealthTable tbody');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&','<':'<','>':'>','"':'"' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function limitText(used,limit){ return Number.isFinite(limit) ? (String(used ?? 0)+' / '+String(limit)) : String(used ?? 0); }function renderAuthQuotaTable(quota){ const keys=Array.isArray(quota?.keys) ? quota.keys : []; if(!keys.length){ authQuotaTableBody.innerHTML='<tr><td colspan="6" class="muted">No managed keys configured</td></tr>'; return; } authQuotaTableBody.innerHTML=keys.map(item=>{ const usage=item.usage || {}; const quotaCfg=item.quota || {}; const keyName=esc(item.label || item.id || '-')+'<div class="muted"><code>'+esc(item.id || '-')+'</code></div>'; const statusClass=item.status === 'exhausted' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const windowText=quotaCfg.window_seconds ? (esc(quotaCfg.window_seconds)+'s'+(usage.windowResetAt ? '<div class="muted">reset '+esc(String(usage.windowResetAt).replace('T',' ').replace('.000Z','Z'))+'</div>' : '<div class="muted">not started</div>')) : '-'; return '<tr><td>'+keyName+'</td><td>'+esc((item.scopes || []).join(', ') || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(item.status || '-')+'</span></td><td>'+esc(limitText(usage.requestsUsed,usage.requestLimit))+'</td><td>'+esc(limitText(usage.tokensUsed,usage.tokenLimit))+'</td><td>'+windowText+'</td></tr>'; }).join('');}function renderModelPoolHealth(data){ const summary=data?.summary || {}; const pools=Array.isArray(data?.pools) ? data.pools : []; const statusClass=summary.open ? 'critical' : (summary.cooldown ? 'warn' : 'info'); const averageLatency=Number.isFinite(summary.averageLatencyMs) ? (Number(summary.averageLatencyMs).toFixed(0)+' ms avg') : 'no latency samples'; modelPoolHealthSummary.className='alert '+statusClass; modelPoolHealthSummary.innerHTML='<strong>Pool health: '+esc(summary.healthy || 0)+' healthy / '+esc(summary.cooldown || 0)+' cooldown / '+esc(summary.open || 0)+' open</strong><div class="muted">'+esc(summary.pools || 0)+' pools \xB7 '+esc(summary.endpoints || 0)+' endpoints \xB7 '+esc(averageLatency)+' \xB7 persisted endpoints '+esc(data?.persistedState?.endpoints || 0)+'</div>'; const rows=[]; pools.forEach(pool=>{ (pool.endpoints || []).forEach(endpoint=>{ const recovery=endpoint.circuitOpenUntil ? ('circuit opens until '+new Date(endpoint.circuitOpenUntil).toISOString()) : endpoint.cooldownUntil ? ('cooldown until '+new Date(endpoint.cooldownUntil).toISOString()) : '-'; const latency=endpoint.latency ? (Number(endpoint.latency.averageMs || 0).toFixed(0)+' ms avg / '+esc(endpoint.latency.sampleCount || 0)+' samples') : '-'; const endpointLabel='<code>'+esc(endpoint.id || '-')+'</code>'+(endpoint.active ? ' <span class="pill info">active</span>' : '')+'<div class="muted">'+esc(endpoint.providerName || '-')+' / '+esc(endpoint.upstreamServiceId || endpoint.upstreamBaseUrl || 'local')+'</div>'; const statusCls=endpoint.status === 'open' ? 'critical' : (endpoint.status === 'cooldown' ? 'warn' : 'info'); rows.push('<tr><td><code>'+esc(pool.modelId || '-')+'</code><div class="muted">'+esc(pool.strategy || '-')+'</div></td><td>'+endpointLabel+'</td><td><span class="pill '+statusCls+'">'+esc(endpoint.status || '-')+'</span></td><td>'+esc(latency)+'</td><td>'+esc(endpoint.failureCount || 0)+'<div class="muted">success '+esc(endpoint.successCount || 0)+'</div></td><td>'+esc(endpoint.lastSuccessAt ? new Date(endpoint.lastSuccessAt).toISOString() : '-')+'</td><td>'+esc(recovery)+'</td></tr>'); }); }); modelPoolHealthTableBody.innerHTML=rows.length ? rows.join('') : '<tr><td colspan="7" class="muted">No registration model pools configured</td></tr>';}async function loadModelPoolHealth(){ const res=await fetch('/api/models/pool-health'); const data=await res.json(); renderModelPoolHealth(data);}function renderRoleConnectionGuide(data){ const listener=data.listener || {}; const connection=data.clientConnection || {}; const mode=data.runtimeMode || '-'; const role=data.serviceRole || '-'; const listenerText=listener.host ? (listener.host+':'+(listener.port || '-')+(listener.public ? ' (public)' : ' (local)')) : '-'; const connectionText=connection.baseUrl ? (connection.baseUrl+' \xB7 '+(Array.isArray(connection.recommendedScopes) ? connection.recommendedScopes.join(' + ') : '')) : (connection.guidance || '-'); listenerStatusSummary.textContent=listenerText; roleConnectionSummary.textContent=mode+' / '+role; listenerConnectionSummary.textContent=listenerText; clientConnectionSummary.textContent=connectionText || '-';}function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">Router.default \u548C\u8DEF\u7531\u89C4\u5219\u5F15\u7528\u8FD9\u4E2A model id\uFF1B\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 interface\uFF1B\u65E7 protocol \u4F1A\u81EA\u52A8\u8BFB\u53D6\u4E3A\u517C\u5BB9\u503C\u3002</div></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4E0A\u6E38\u771F\u5B9E\u6A21\u578B\u540D\uFF0C\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 api\uFF1B\u65E7 api_base_url \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 key\uFF1B\u65E7 api_key \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Context window</label><input data-field="context_window_tokens" data-index="'+index+'" value="'+esc(model.metadata?.context_window_tokens || '')+'" placeholder="200000"></div>' + '<div><label>Safe input</label><input data-field="safe_input_tokens" data-index="'+index+'" value="'+esc(model.metadata?.safe_input_tokens || '')+'" placeholder="180000"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const contextWindowTokens=(read('context_window_tokens')?.value || '').trim(); const safeInputTokens=(read('safe_input_tokens')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(contextWindowTokens){ metadata.context_window_tokens=Number(contextWindowTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'context_window_tokens')){ delete metadata.context_window_tokens; } if(safeInputTokens){ metadata.safe_input_tokens=Number(safeInputTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'safe_input_tokens')){ delete metadata.safe_input_tokens; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); const modelPoolEntries=Object.entries(data.modelPools || {}); const modelPoolEndpointCount=modelPoolEntries.reduce((sum,[_modelId,pool])=>sum+((pool.endpoints || []).length),0); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04 / '+modelPoolEntries.length+' \u4E2A model pool / '+modelPoolEndpointCount+' \u4E2A pool endpoint'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; compiledModelPoolsTableBody.innerHTML=modelPoolEntries.length ? modelPoolEntries.map(([modelId,pool])=>{ const endpoints=pool.endpoints || []; return '<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td>'+esc(pool.strategy || '-')+'</td>' + '<td><code>'+esc(pool.activeEndpointId || '-')+'</code></td>' + '<td>'+endpoints.map(endpoint=>{ const latency=endpoint.health?.latency; return '<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+' / '+esc(endpoint.health?.status || 'healthy')+(latency ? ' / avg '+esc(Math.round(latency.averageMs))+'ms' : '')+'</span><div class="muted">'+esc(endpoint.upstreamServiceId || endpoint.api || '-')+'</div></div>'; }).join('')+'</td>' + '<td>'+((pool.warnings || []).length ? pool.warnings.map(w=>'<div class="warning-text">'+esc(w)+'</div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No compiled model pools</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [serviceRes,remoteRes]=await Promise.all([fetch('/api/service-info'),fetch('/api/remote-status')]); const data=await serviceRes.json(); const remoteData=await remoteRes.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; serviceModeStatus.textContent=data.runtimeMode || '-'; serviceRoleStatus.textContent=data.serviceRole || '-'; renderRoleConnectionGuide(data); const auth=data.auth || {}; const managed=auth.managedKeys || {}; const quota=auth.quota || {}; const quotaText=Number.isFinite(quota.requestsUsed) ? (' \xB7 quota '+quota.requestsUsed+' req'+(quota.windowResetAt ? ' \xB7 reset '+String(quota.windowResetAt).replace('T',' ').replace('.000Z','Z') : '')) : ''; authStatusSummary.textContent=auth.required ? ((auth.bootstrapConfigured ? 'bootstrap' : 'managed')+' \xB7 '+(managed.active ?? 0)+' active'+quotaText) : 'not configured'; renderAuthQuotaTable(quota); const security=data.security || {}; const issues=Array.isArray(security.issues) ? security.issues : []; securityStatusSummary.textContent=security.status || '-'; securitySummary.className='alert '+((security.status === 'critical') ? 'critical' : (security.status === 'warning' ? 'warn' : 'info')); securitySummary.innerHTML='<strong>Security: '+esc(security.status || '-')+'</strong><div>'+esc(issues[0]?.message || '\u5F53\u524D\u670D\u52A1\u672A\u53D1\u73B0\u660E\u663E\u9274\u6743\u66B4\u9732\u98CE\u9669')+'</div>'+ (issues.length ? '<ul class="mini-list">'+issues.map(issue=>'<li>'+esc(issue.action || issue.code)+'</li>').join('')+'</ul>' : ''); const registration=data.registration || {}; registrationStatusSummary.textContent=registration.enabled ? ((registration.models ?? 0)+' models / '+(registration.upstreamServices ?? 0)+' upstream') : 'disabled'; const remote=remoteData.remote || {}; remoteStatusSummary.textContent=remote.enabled ? ((remote.ready ? 'ready' : (remote.reachable ? 'reachable' : 'unreachable'))+' \xB7 '+(remote.baseUrl || '-')) : 'disabled'; const remoteRegistration=remoteData.remoteRegistration || {}; const remoteRegistrationSummary=remoteRegistration.summary || {}; remoteRegistrationStatusSummary.textContent=remoteRegistration.enabled ? (remoteRegistration.available ? (remoteRegistration.registrationEnabled ? ((remoteRegistrationSummary.models ?? 0)+' remote models / '+(remoteRegistrationSummary.upstreamServices ?? 0)+' upstream') : 'remote registration disabled') : ('unavailable \xB7 '+(remoteRegistration.error || remoteRegistration.baseUrl || '-'))) : 'disabled'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } try { await loadModelPoolHealth(); } catch (_poolError) { modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });healthSummary.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-health-action]'); if(btn){ applyHealthAction(btn.dataset.healthAction); } });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics,health,outcome){ metricsGrid.innerHTML=[ ['Health', health?.status || 'idle'], ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Model switch rate', pct(outcome?.modelSwitchRate)], ['Alignment on switch', pct(outcome?.alignmentOnSwitchRate)], ['Context fallback', pct(outcome?.contextWindowFallbackRate)], ['Context exceeded', pct(outcome?.contextWindowExceededRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderOutcomeGroups(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code><span class="muted"> \xB7 '+esc(item.totalTraces)+' traces</span></span><strong>switch '+esc(pct(item.modelSwitchRate))+' \xB7 align '+esc(pct(item.alignmentOnSwitchRate))+' \xB7 cascade '+esc(pct(item.cascadeAfterSwitchRate))+' \xB7 '+esc(fmt(item.averageLatencyMs))+' ms</strong></li>').join('');}function renderRoutingTuning(items){ if(!items || !items.length){ routingTuningList.innerHTML='<li><span class="muted">No routing tuning recommendations</span><strong>healthy</strong></li>'; return; } routingTuningList.innerHTML=items.map(item=>'<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.code || '-')+'</strong><div class="muted">'+esc(item.message || '')+'</div><div class="muted">'+esc(item.evidence || '')+'</div></span><strong>'+esc(item.action || '')+'</strong></li>').join('');}function renderQualityEvidence(summary){ const items=summary?.samples || []; qualityEvidenceSummary.innerHTML=[['Samples',summary?.totalSamples || 0],['Risk',summary?.failureSamples || 0],['Improvement',summary?.improvementSamples || 0],['Speed risk',summary?.speedRiskSamples || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ qualityEvidenceList.innerHTML='<li><span class="muted">No quality evidence samples</span><strong>0</strong></li>'; return; } qualityEvidenceList.innerHTML=items.map(item=>'<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.type || '-')+'</strong><div class="muted">'+esc(item.requestId || '')+' \xB7 '+esc((item.routeReason || []).join(' / '))+'</div><div class="muted">'+esc(item.evidence || '')+'</div></span><strong>'+esc(item.action || '')+'</strong></li>').join('');}function renderTaskComparison(summary){ const items=summary?.comparisons || []; taskComparisonSummary.innerHTML=[['Tasks',summary?.totalComparedTasks || 0],['Traces',summary?.totalComparedTraces || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ taskComparisonList.innerHTML='<li><span class="muted">No comparable task samples</span><strong>0</strong></li>'; return; } taskComparisonList.innerHTML=items.map(item=>'<li><span><strong>'+esc(item.taskKey || '-')+'</strong><div class="muted">best '+esc(item.bestModel || '-')+' \xB7 baseline '+esc(item.baselineModel || '-')+' \xB7 fastest '+esc(item.fastestModel || '-')+'</div><div class="muted">failure lift '+esc(pct(item.failureRateDelta || 0))+' \xB7 latency lift '+esc(fmt(item.latencyDeltaMs || 0))+' ms \xB7 models '+esc(item.modelCount || 0)+'</div></span><strong>'+esc(item.totalTraces || 0)+' traces</strong></li>').join('');}function renderAnomalies(anomalies,health){ const status=health?.status || 'idle'; const message=health?.message || 'No governance traces yet.'; const actions=Array.isArray(health?.actions) ? health.actions : []; healthSummary.className='alert '+esc(status === 'critical' ? 'critical' : (status === 'watch' ? 'warn' : 'info')); healthSummary.innerHTML='<strong>Health: '+esc(status)+'</strong><div>'+esc(message)+'</div>'+ (actions.length ? '<ul class="mini-list">'+actions.map(action=>'<li><button type="button" data-health-action="'+esc(action)+'">'+esc(action)+'</button></li>').join('')+'</ul>' : ''); if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function applyHealthAction(action){ const text=String(action || '').toLowerCase(); const routeReasonInput=document.getElementById('routeReason'); const cascadeSelect=document.getElementById('cascadeTriggered'); const shadowSelect=document.getElementById('shadowChecked'); if(text.includes('cascade')){ cascadeSelect.value='true'; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered cascade traces'; } else if(text.includes('shadow')){ shadowSelect.value='true'; cascadeSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered shadow traces'; } else { cascadeSelect.value=''; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: showing recent traces'; } loadTraces(); document.getElementById('traceTable').scrollIntoView({ behavior:'smooth', block:'start' });}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes,healthRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query), fetch('/api/governance/health'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); const healthData=await healthRes.json(); const health=healthData.health || metricsData.health; renderMetrics(metricsData.metrics || {},health,metricsData.outcome || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRoutingTuning(health?.routingTuning || []); renderQualityEvidence(metricsData.qualityEvidence || {}); renderTaskComparison(metricsData.taskComparison || {}); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderOutcomeGroups(routeOutcomeRanking,metricsData.outcome?.byRouteReason || [],'No route outcomes'); renderOutcomeGroups(modelOutcomeRanking,metricsData.outcome?.byFinalModel || [],'No model outcomes'); renderOutcomeGroups(intentOutcomeRanking,metricsData.outcome?.bySemanticIntent || [],'No intent outcomes'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
|
|
5619
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:.75rem 1rem;flex-wrap:wrap;align-items:flex-start;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.scope-guide{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:.75rem;margin-top:.75rem}.scope-guide div{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem}.scope-guide strong{display:block;margin-bottom:.35rem}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}.pill.info{background:#eff6ff;color:#1d4ed8}.pill.warn{background:#fff7ed;color:#9a3412}.pill.critical{background:#fef2f2;color:#991b1b}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Mode</span><strong id="serviceModeStatus">${escapedRuntimeMode}</strong></div><div class="status-tile"><span class="muted">Role</span><strong id="serviceRoleStatus">${escapedServiceRole}</strong></div><div class="status-tile"><span class="muted">Listener</span><strong id="listenerStatusSummary">${escapedListenerSummary}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div><div class="status-tile"><span class="muted">Remote service</span><strong id="remoteStatusSummary">${escapedRemoteSummary}</strong></div><div class="status-tile"><span class="muted">Remote registration</span><strong id="remoteRegistrationStatusSummary">checking</strong></div><div class="status-tile"><span class="muted">Registration</span><strong id="registrationStatusSummary">${escapedRegistrationSummary}</strong></div><div class="status-tile"><span class="muted">Auth</span><strong id="authStatusSummary">${escapedAuthSummary}</strong></div><div class="status-tile"><span class="muted">Security</span><strong id="securityStatusSummary">${escapedSecuritySummary}</strong></div></div></div></div><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="subpanel"><div class="row"><strong>Current Router slots</strong><span class="muted">\u89E3\u91CA\u57FA\u7840\u8DEF\u7531\u69FD\u4F4D\u5F15\u7528\u7684 modelId\u3001\u4E0A\u6E38\u6A21\u578B\u3001\u80FD\u529B\u548C\u6F5C\u5728\u914D\u7F6E\u98CE\u9669</span></div><div id="routerSlotSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Configured slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Resolved slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Warnings</span><strong>0</strong></div></div><table id="routerSlotTable" class="management-table"><thead><tr><th>Slot</th><th>When used</th><th>Model ref</th><th>Resolved target</th><th>Capabilities</th><th>Warning</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading router slot explanation...</td></tr></tbody></table><div id="contextWindowGuide" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Context window guide</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u4E0A\u4E0B\u6587\u7A97\u53E3\u4E0E Router.longContext \u5EFA\u8BAE</div></div></div></div><div class="subpanel"><div class="row"><strong>SmartRouter explanation</strong><span class="muted">\u5C55\u793A\u89C4\u5219\u547D\u4E2D\u987A\u5E8F\u3001\u5019\u9009\u6A21\u578B\u3001router_model\u3001semantic/sticky \u5F00\u5173\u4E0E fallback</span></div><div id="smartRouterExplanationSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Enabled</span><strong>-</strong></div><div class="diff-chip"><span class="muted">Rules</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Candidates</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Warnings</span><strong>0</strong></div></div><div id="smartRouterRouteOrder" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Route order</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A SmartRouter \u51B3\u7B56\u987A\u5E8F\u3002</div></div></div><table id="smartRouterRulesTable" class="management-table"><thead><tr><th>Order</th><th>Rule</th><th>Model</th><th>Patterns</th><th>Semantic</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading SmartRouter rules...</td></tr></tbody></table><table id="smartRouterCandidatesTable" class="management-table"><thead><tr><th>Order</th><th>Candidate</th><th>Description</th><th>Status</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading SmartRouter candidates...</td></tr></tbody></table><div id="smartCandidateGuide" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Candidate guide</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u63D0\u793A fast / balanced / deep / long-context \u5019\u9009\u8986\u76D6\u3002</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div class="alert info"><strong>Models field guide</strong><div class="muted">\u65B0\u914D\u7F6E\u8BF7\u4F7F\u7528\u5165\u53E3\u5B57\u6BB5\uFF1Aid / api / key / interface / model / thinking / metadata\uFF1Bapi_key / api_base_url / protocol \u4EC5\u4F5C\u4E3A\u65E7\u914D\u7F6E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" aria-label="JSON config draft" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4","thinking":"auto","metadata":{"vendor_hint":"openrouter"}}],"Router":{"default":"sonnet"}}'></textarea><div class="muted">JSON \u8349\u7A3F\u540C\u6837\u5EFA\u8BAE\u53EA\u5199\u5165\u53E3\u5B57\u6BB5\uFF1B\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u5F52\u4E00\uFF0C\u65E7\u5B57\u6BB5\u522B\u540D\u65E0\u9700\u624B\u52A8\u8865\u5145\u3002</div><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model pools</strong><span class="muted">Registration.models \u7F16\u8BD1\u51FA\u7684\u540C\u6A21\u578B\u591A\u6E90\u6C60\uFF0C\u5F53\u524D\u652F\u6301 priority / least-latency active endpoint\u3001\u975E\u6D41\u5F0F\u9519\u8BEF fallback\u3001\u5185\u5B58 health/cooldown\u3001\u7194\u65AD\u72B6\u6001\u4E0E\u5EF6\u8FDF\u7A97\u53E3</span></div><table id="compiledModelPoolsTable" class="management-table"><thead><tr><th>Pool</th><th>Strategy</th><th>Active endpoint</th><th>Endpoints</th><th>Warnings</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading model pools...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div id="securitySummary" class="alert info"><strong>Security pending</strong><div class="muted">\u7B49\u5F85\u670D\u52A1\u5B89\u5168\u72B6\u6001\u52A0\u8F7D</div></div><div class="subpanel" id="roleConnectionGuide"><div class="row"><strong>Role & connection guide</strong><span class="muted">\u6309\u5F53\u524D local / server / cloud \u89D2\u8272\u786E\u8BA4\u76D1\u542C\u5730\u5740\u3001\u7EF4\u62A4\u5165\u53E3\u548C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\u65B9\u5F0F\u3002</span></div><div class="scope-guide"><div><strong>current role</strong><span id="roleConnectionSummary" class="muted">${escapedRuntimeMode} / ${escapedServiceRole}</span></div><div><strong>listener</strong><span id="listenerConnectionSummary" class="muted">${escapedListenerSummary}</span></div><div><strong>remote clients</strong><span id="clientConnectionSummary" class="muted">${escapedClientConnectionSummary}</span></div></div><div class="muted" style="margin-top:.75rem">${escapedLocalUserRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedServerMaintainerRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedRemoteClientRoleGuide}</div></div><div class="subpanel" id="authScopeGuide"><div class="row"><strong>Auth scope guide</strong><span class="muted">\u6309\u7528\u9014\u53D1\u653E\u6700\u5C0F\u6743\u9650 key\uFF0C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u4E0D\u8981\u590D\u7528 admin key\u3002</span></div><div class="scope-guide"><div><strong>admin</strong><span class="muted">\u670D\u52A1\u6240\u6709\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001auth \u7BA1\u7406\uFF0C\u4EE5\u53CA\u6240\u6709\u8FD0\u7EF4\u5199\u64CD\u4F5C\u3002</span></div><div><strong>operator</strong><span class="muted">\u65E5\u5E38\u8FD0\u7EF4\u4F7F\u7528\uFF1A\u91CD\u542F\u3001\u6CBB\u7406\u5FEB\u7167\u3001\u5B9A\u65F6\u5FEB\u7167\u3001\u5F02\u5E38\u9608\u503C\u548C\u5F52\u6863\u5220\u9664\uFF1B\u4E0D\u80FD\u67E5\u770B\u914D\u7F6E\u6216\u7BA1\u7406 auth\u3002</span></div><div><strong>client</strong><span class="muted">\u5BA2\u6237\u7AEF\u6A21\u578B\u8C03\u7528\uFF1A/v1/messages\u3001/v1/chat/completions\uFF1B\u6A21\u578B\u8C03\u7528\u914D\u989D\u53EA\u8BA1\u5165\u8FD9\u91CC\u3002</span></div><div><strong>read-only</strong><span class="muted">\u53EA\u8BFB\u89C2\u6D4B\uFF1Ahealth\u3001service-info\u3001compiled models\u3001model pool health\u3001transformers \u548C governance GET\u3002</span></div><div><strong>client + read-only</strong><span class="muted">\u8FDC\u7A0B token \u540C\u65F6\u9700\u8981 ready/status \u63A2\u6D4B\u4E0E\u6A21\u578B\u8C03\u7528\u65F6\u4F7F\u7528\u8BE5\u7EC4\u5408\u3002</span></div></div><div class="muted" style="margin-top:.75rem">\u7BA1\u7406\u5165\u53E3\uFF1A\u7528 admin key \u8C03\u7528 <code>GET /api/auth/keys</code> \u67E5\u770B\u5217\u8868\uFF0C<code>POST /api/auth/keys</code> \u751F\u6210 key\uFF0C<code>POST /api/auth/keys/:id/revoke</code> \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\uFF0C\u8BF7\u76F4\u63A5\u4EA4\u7ED9\u5BF9\u5E94\u5BA2\u6237\u7AEF\u4FDD\u5B58\u3002</div></div><div class="subpanel"><div class="row"><strong>Auth quota</strong><span class="muted">\u6309 managed key \u67E5\u770B\u6A21\u578B\u8C03\u7528\u914D\u989D\u3001\u5F53\u524D\u7528\u91CF\u4E0E\u7A97\u53E3\u91CD\u7F6E\u65F6\u95F4</span></div><table id="authQuotaTable" class="management-table"><thead><tr><th>Key</th><th>Scope</th><th>Status</th><th>Requests</th><th>Tokens</th><th>Window</th></tr></thead><tbody><tr><td colspan="6" class="muted">Waiting for service status...</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Model pool health</strong><span class="muted">\u67E5\u770B\u540C\u6A21\u578B\u591A\u6E90\u6C60\u7684 active endpoint\u3001\u6301\u4E45\u5316\u72B6\u6001\u3001cooldown\u3001\u7194\u65AD\u4E0E\u5EF6\u8FDF\u7A97\u53E3\u3002</span></div><div id="modelPoolHealthSummary" class="alert info"><strong>Pool health pending</strong><div class="muted">\u7B49\u5F85\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001\u52A0\u8F7D</div></div><table id="modelPoolHealthTable" class="management-table"><thead><tr><th>Pool</th><th>Endpoint</th><th>Status</th><th>Latency</th><th>Failures</th><th>Last success</th><th>Recovery</th></tr></thead><tbody><tr><td colspan="7" class="muted">Waiting for model pool health...</td></tr></tbody></table></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/pool-health</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Health</span><strong>-</strong></div><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Model switch rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment on switch</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="healthSummary" class="alert info"><strong>Health pending</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u5065\u5EB7\u6458\u8981\u52A0\u8F7D</div></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Routing tuning</strong><span class="muted">\u57FA\u4E8E outcome \u8BC1\u636E\u7ED9\u51FA SmartRouter \u8C03\u4F18\u5EFA\u8BAE</span></div><ul id="routingTuningList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Quality evidence</strong><span class="muted">\u771F\u5B9E trace \u4E2D\u7684\u5931\u8D25\u3001\u8FDE\u7EED\u6027\u548C\u901F\u5EA6\u98CE\u9669\u6837\u672C</span></div><div id="qualityEvidenceSummary" class="stats"><div class="stat"><span class="muted">Samples</span><strong>-</strong></div><div class="stat"><span class="muted">Risk</span><strong>-</strong></div><div class="stat"><span class="muted">Improvement</span><strong>-</strong></div><div class="stat"><span class="muted">Speed risk</span><strong>-</strong></div></div><ul id="qualityEvidenceList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Task comparison</strong><span class="muted">\u540C\u7C7B\u4EFB\u52A1\u4E0B\u4E0D\u540C\u6700\u7EC8\u6A21\u578B\u7684\u5931\u8D25\u7387\u548C\u901F\u5EA6\u5BF9\u6BD4</span></div><div id="taskComparisonSummary" class="stats"><div class="stat"><span class="muted">Tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Traces</span><strong>-</strong></div></div><ul id="taskComparisonList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Benchmark summary</strong><span class="muted">\u628A\u6CBB\u7406 trace \u4E0E\u56FA\u5B9A\u4EFB\u52A1\u8BC4\u6D4B\u5165\u53E3\u5408\u5E76\u6210\u7EF4\u62A4\u8005 A/B \u95ED\u73AF</span></div><div id="benchmarkSummary" class="stats"><div class="stat"><span class="muted">Comparable tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Evidence samples</span><strong>-</strong></div><div class="stat"><span class="muted">Best quality lift</span><strong>-</strong></div><div class="stat"><span class="muted">Best speed lift</span><strong>-</strong></div></div><ul id="benchmarkActionList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by route</strong><span class="muted">\u5207\u6362\u3001alignment\u3001cascade \u4E0E\u5EF6\u8FDF</span></div><ul id="routeOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by model</strong><span class="muted">\u6700\u7EC8\u6A21\u578B\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="modelOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by intent</strong><span class="muted">\u4EFB\u52A1\u610F\u56FE\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="intentOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Recent route decisions</strong><span class="muted">\u628A\u6700\u8FD1\u8BF7\u6C42\u7684 route source\u3001\u89C4\u5219\u3001\u8BED\u4E49\u610F\u56FE\u3001\u7F6E\u4FE1\u5EA6\u548C fallback \u539F\u56E0\u7FFB\u8BD1\u6210\u53EF\u8BFB\u6458\u8981\u3002</span></div><ul id="routeDecisionSummaryList" class="mini-list"><li><span class="muted">Loading route decisions</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Recent switch continuity</strong><span class="muted">\u89E3\u91CA\u6700\u8FD1\u8BF7\u6C42\u662F\u5426\u5207\u6362\u6A21\u578B\u3001\u662F\u5426\u8865\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u53CA\u5207\u6362\u540E\u662F\u5426\u89E6\u53D1 cascade\u3002</span></div><ul id="switchContinuitySummaryList" class="mini-list"><li><span class="muted">Loading switch continuity</span><strong>-</strong></li></ul></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\u6458\u8981</li><li><code>GET /api/auth/audit</code> \u2014 \u67E5\u770B\u9274\u6743\u5BA1\u8BA1\u6458\u8981</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const routerSlotSummary=document.getElementById('routerSlotSummary');const routerSlotTableBody=document.querySelector('#routerSlotTable tbody');const contextWindowGuide=document.getElementById('contextWindowGuide');const smartRouterExplanationSummary=document.getElementById('smartRouterExplanationSummary');const smartRouterRouteOrder=document.getElementById('smartRouterRouteOrder');const smartRouterRulesTableBody=document.querySelector('#smartRouterRulesTable tbody');const smartRouterCandidatesTableBody=document.querySelector('#smartRouterCandidatesTable tbody');const smartCandidateGuide=document.getElementById('smartCandidateGuide');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const listenerStatusSummary=document.getElementById('listenerStatusSummary');const roleConnectionSummary=document.getElementById('roleConnectionSummary');const listenerConnectionSummary=document.getElementById('listenerConnectionSummary');const clientConnectionSummary=document.getElementById('clientConnectionSummary');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const authStatusSummary=document.getElementById('authStatusSummary');const securityStatusSummary=document.getElementById('securityStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const compiledModelPoolsTableBody=document.querySelector('#compiledModelPoolsTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const routeOutcomeRanking=document.getElementById('routeOutcomeRanking');const modelOutcomeRanking=document.getElementById('modelOutcomeRanking');const intentOutcomeRanking=document.getElementById('intentOutcomeRanking');const healthSummary=document.getElementById('healthSummary');const routingTuningList=document.getElementById('routingTuningList');const qualityEvidenceSummary=document.getElementById('qualityEvidenceSummary');const qualityEvidenceList=document.getElementById('qualityEvidenceList');const taskComparisonSummary=document.getElementById('taskComparisonSummary');const taskComparisonList=document.getElementById('taskComparisonList');const benchmarkSummary=document.getElementById('benchmarkSummary');const benchmarkActionList=document.getElementById('benchmarkActionList');const securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable tbody');const modelPoolHealthSummary=document.getElementById('modelPoolHealthSummary');const modelPoolHealthTableBody=document.querySelector('#modelPoolHealthTable tbody');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let lastCompiledModelsData=null;let activeValidationHighlight=null;function withDraftCompiledData(payload){ return { ...(lastCompiledModelsData || {}), normalizedConfig: payload || currentDraftConfig || {} }; }const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&','<':'<','>':'>','"':'"' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function limitText(used,limit){ return Number.isFinite(limit) ? (String(used ?? 0)+' / '+String(limit)) : String(used ?? 0); }function renderAuthQuotaTable(quota){ const keys=Array.isArray(quota?.keys) ? quota.keys : []; if(!keys.length){ authQuotaTableBody.innerHTML='<tr><td colspan="6" class="muted">No managed keys configured</td></tr>'; return; } authQuotaTableBody.innerHTML=keys.map(item=>{ const usage=item.usage || {}; const quotaCfg=item.quota || {}; const keyName=esc(item.label || item.id || '-')+'<div class="muted"><code>'+esc(item.id || '-')+'</code></div>'; const statusClass=item.status === 'exhausted' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const windowText=quotaCfg.window_seconds ? (esc(quotaCfg.window_seconds)+'s'+(usage.windowResetAt ? '<div class="muted">reset '+esc(String(usage.windowResetAt).replace('T',' ').replace('.000Z','Z'))+'</div>' : '<div class="muted">not started</div>')) : '-'; return '<tr><td>'+keyName+'</td><td>'+esc((item.scopes || []).join(', ') || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(item.status || '-')+'</span></td><td>'+esc(limitText(usage.requestsUsed,usage.requestLimit))+'</td><td>'+esc(limitText(usage.tokensUsed,usage.tokenLimit))+'</td><td>'+windowText+'</td></tr>'; }).join('');}function renderModelPoolHealth(data){ const summary=data?.summary || {}; const pools=Array.isArray(data?.pools) ? data.pools : []; const statusClass=summary.open ? 'critical' : (summary.cooldown ? 'warn' : 'info'); const averageLatency=Number.isFinite(summary.averageLatencyMs) ? (Number(summary.averageLatencyMs).toFixed(0)+' ms avg') : 'no latency samples'; modelPoolHealthSummary.className='alert '+statusClass; modelPoolHealthSummary.innerHTML='<strong>Pool health: '+esc(summary.healthy || 0)+' healthy / '+esc(summary.cooldown || 0)+' cooldown / '+esc(summary.open || 0)+' open</strong><div class="muted">'+esc(summary.pools || 0)+' pools \xB7 '+esc(summary.endpoints || 0)+' endpoints \xB7 '+esc(averageLatency)+' \xB7 persisted endpoints '+esc(data?.persistedState?.endpoints || 0)+'</div>'; const rows=[]; pools.forEach(pool=>{ (pool.endpoints || []).forEach(endpoint=>{ const recovery=endpoint.circuitOpenUntil ? ('circuit opens until '+new Date(endpoint.circuitOpenUntil).toISOString()) : endpoint.cooldownUntil ? ('cooldown until '+new Date(endpoint.cooldownUntil).toISOString()) : '-'; const latency=endpoint.latency ? (Number(endpoint.latency.averageMs || 0).toFixed(0)+' ms avg / '+esc(endpoint.latency.sampleCount || 0)+' samples') : '-'; const endpointLabel='<code>'+esc(endpoint.id || '-')+'</code>'+(endpoint.active ? ' <span class="pill info">active</span>' : '')+'<div class="muted">'+esc(endpoint.providerName || '-')+' / '+esc(endpoint.upstreamServiceId || endpoint.upstreamBaseUrl || 'local')+'</div>'; const statusCls=endpoint.status === 'open' ? 'critical' : (endpoint.status === 'cooldown' ? 'warn' : 'info'); rows.push('<tr><td><code>'+esc(pool.modelId || '-')+'</code><div class="muted">'+esc(pool.strategy || '-')+'</div></td><td>'+endpointLabel+'</td><td><span class="pill '+statusCls+'">'+esc(endpoint.status || '-')+'</span></td><td>'+esc(latency)+'</td><td>'+esc(endpoint.failureCount || 0)+'<div class="muted">success '+esc(endpoint.successCount || 0)+'</div></td><td>'+esc(endpoint.lastSuccessAt ? new Date(endpoint.lastSuccessAt).toISOString() : '-')+'</td><td>'+esc(recovery)+'</td></tr>'); }); }); modelPoolHealthTableBody.innerHTML=rows.length ? rows.join('') : '<tr><td colspan="7" class="muted">No registration model pools configured</td></tr>';}async function loadModelPoolHealth(){ const res=await fetch('/api/models/pool-health'); const data=await res.json(); renderModelPoolHealth(data);}function renderRoleConnectionGuide(data){ const listener=data.listener || {}; const connection=data.clientConnection || {}; const mode=data.runtimeMode || '-'; const role=data.serviceRole || '-'; const listenerText=listener.host ? (listener.host+':'+(listener.port || '-')+(listener.public ? ' (public)' : ' (local)')) : '-'; const connectionText=connection.baseUrl ? (connection.baseUrl+' \xB7 '+(Array.isArray(connection.recommendedScopes) ? connection.recommendedScopes.join(' + ') : '')) : (connection.guidance || '-'); listenerStatusSummary.textContent=listenerText; roleConnectionSummary.textContent=mode+' / '+role; listenerConnectionSummary.textContent=listenerText; clientConnectionSummary.textContent=connectionText || '-';}function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">Router.default \u548C\u8DEF\u7531\u89C4\u5219\u5F15\u7528\u8FD9\u4E2A model id\uFF1B\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 interface\uFF1B\u65E7 protocol \u4F1A\u81EA\u52A8\u8BFB\u53D6\u4E3A\u517C\u5BB9\u503C\u3002</div></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4E0A\u6E38\u771F\u5B9E\u6A21\u578B\u540D\uFF0C\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 api\uFF1B\u65E7 api_base_url \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 key\uFF1B\u65E7 api_key \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Context window</label><input data-field="context_window_tokens" data-index="'+index+'" value="'+esc(model.metadata?.context_window_tokens || '')+'" placeholder="200000"></div>' + '<div><label>Safe input</label><input data-field="safe_input_tokens" data-index="'+index+'" value="'+esc(model.metadata?.safe_input_tokens || '')+'" placeholder="180000"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const contextWindowTokens=(read('context_window_tokens')?.value || '').trim(); const safeInputTokens=(read('safe_input_tokens')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(contextWindowTokens){ metadata.context_window_tokens=Number(contextWindowTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'context_window_tokens')){ delete metadata.context_window_tokens; } if(safeInputTokens){ metadata.safe_input_tokens=Number(safeInputTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'safe_input_tokens')){ delete metadata.safe_input_tokens; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyContextWindowAction(action,modelId){ if(action!=='set-long-context' || !modelId){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u8BE5\u4E0A\u4E0B\u6587\u7A97\u53E3\u64CD\u4F5C'; return; } const payload=buildDraftPayloadFromForm(); payload.Router={ ...(payload.Router || {}), longContext:modelId }; currentDraftConfig=payload; renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06 Router.longContext \u8BBE\u7F6E\u4E3A '+modelId+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function getRouterSlotDefinitions(){ return [ { key:'default', label:'Default', when:'\u666E\u901A\u8BF7\u6C42\u3001\u89C4\u5219\u672A\u547D\u4E2D\u6216\u5176\u4ED6\u69FD\u4F4D\u672A\u914D\u7F6E\u65F6\u4F7F\u7528', required:true }, { key:'think', label:'Thinking', when:'\u8BF7\u6C42\u5305\u542B thinking \u65F6\u4F18\u5148\u4F7F\u7528', required:false }, { key:'longContext', label:'Long context', when:'\u8F93\u5165\u8D85\u8FC7\u9608\u503C\uFF0C\u6216\u5F53\u524D\u6A21\u578B safe_input_tokens \u4E0D\u591F\u65F6\u4F7F\u7528', required:false }, { key:'background', label:'Background', when:'Claude Code \u8F7B\u91CF\u540E\u53F0\u6A21\u578B\u8BF7\u6C42\u65F6\u4F7F\u7528', required:false }, { key:'webSearch', label:'Web search', when:'\u8BF7\u6C42\u5305\u542B web_search \u5DE5\u5177\u65F6\u4F7F\u7528', required:false }, ];}function renderRouterSlotExplanation(data){ const config=data?.normalizedConfig || { Router:(currentDraftConfig.Router && Object.keys(currentDraftConfig.Router).length ? currentDraftConfig.Router : (data?.router || {})) }; const router=config.Router || {}; const modelMap=data?.modelMap || {}; const slots=getRouterSlotDefinitions(); let configured=0; let resolved=0; let warnings=0; const defaultRef=String(router.default || '').trim(); const defaultModel=defaultRef ? modelMap[defaultRef] : null; const rows=slots.map(slot=>{ const ref=String(router[slot.key] || '').trim(); const model=ref ? modelMap[ref] : null; const caps=model?.capabilities || {}; const slotWarnings=[]; if(ref){ configured+=1; } if(ref && model){ resolved+=1; } if(slot.required && !ref){ slotWarnings.push('\u5FC5\u586B\u69FD\u4F4D\u672A\u914D\u7F6E'); } if(ref && !model){ slotWarnings.push('\u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id'); } if(slot.key==='think' && model && caps.thinking?.supported === false){ slotWarnings.push('\u76EE\u6807\u6A21\u578B\u58F0\u660E\u4E0D\u652F\u6301 reasoning'); } if(slot.key==='longContext' && model){ if(!caps.contextWindowTokens){ slotWarnings.push('\u7F3A\u5C11 context_window_tokens'); } if(!caps.safeInputTokens){ slotWarnings.push('\u7F3A\u5C11 safe_input_tokens'); } if(defaultModel?.capabilities?.contextWindowTokens && caps.contextWindowTokens && caps.contextWindowTokens <= defaultModel.capabilities.contextWindowTokens){ slotWarnings.push('\u7A97\u53E3\u4E0D\u9AD8\u4E8E default'); } } if(model && slot.key!=='longContext' && (!caps.contextWindowTokens || !caps.safeInputTokens)){ slotWarnings.push('\u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E'); } warnings+=slotWarnings.length; const target=model ? ('<code>'+esc(model.providerName || '-')+'</code><div class="muted">'+esc(model.modelName || '-')+'</div>') : '<span class="muted">-</span>'; const capabilityParts=model ? [ 'thinking '+(caps.thinking?.supported === false ? 'off' : 'on'), 'tools '+(caps.tools === false ? 'off' : 'on'), 'images '+(caps.images === false ? 'off' : 'on'), caps.contextWindowTokens ? ('ctx '+caps.contextWindowTokens) : 'ctx ?', caps.safeInputTokens ? ('safe '+caps.safeInputTokens) : 'safe ?', ] : []; const warningText=slotWarnings.length ? slotWarnings.join('\uFF1B') : (ref ? 'ok' : '\u672A\u914D\u7F6E\u65F6\u56DE\u5230 default'); const warningClass=slotWarnings.length ? 'warn' : 'info'; return '<tr>' + '<td><strong>'+esc(slot.label)+'</strong><div class="muted">Router.'+esc(slot.key)+'</div></td>' + '<td>'+esc(slot.when)+'</td>' + '<td>'+(ref ? '<code>'+esc(ref)+'</code>' : '<span class="muted">not configured</span>')+'</td>' + '<td>'+target+'</td>' + '<td>'+(capabilityParts.length ? capabilityParts.map(item=>'<span class="pill">'+esc(item)+'</span>').join(' ') : '<span class="muted">-</span>')+'</td>' + '<td><span class="pill '+warningClass+'">'+esc(warningText)+'</span></td>' + '</tr>'; }); routerSlotSummary.innerHTML=[['Configured slots',configured],['Resolved slots',resolved],['Warnings',warnings]].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); routerSlotTableBody.innerHTML=rows.join('');}function readModelMetadataNumber(model,key){ const value=model?.metadata?.[key]; return Number.isFinite(Number(value)) && Number(value)>0 ? Number(value) : undefined;}function getContextWindowEntries(data,config){ const modelMap=data?.modelMap || {}; const draftModels=Array.isArray(config?.Models) ? config.Models : []; if(draftModels.length){ return draftModels.map(model=>{ const id=String(model?.id || '').trim(); const compiled=id ? modelMap[id] : null; const caps=compiled?.capabilities || {}; return { id, modelName:model?.model || compiled?.modelName || '-', contextWindowTokens:readModelMetadataNumber(model,'context_window_tokens') || caps.contextWindowTokens, safeInputTokens:readModelMetadataNumber(model,'safe_input_tokens') || caps.safeInputTokens }; }).filter(item=>item.id); } return Object.entries(modelMap).map(([id,model])=>({ id, modelName:model?.modelName || '-', contextWindowTokens:model?.capabilities?.contextWindowTokens, safeInputTokens:model?.capabilities?.safeInputTokens }));}function renderContextWindowGuide(data){ const config=data?.normalizedConfig || currentDraftConfig || {}; const router=config.Router || {}; const entries=getContextWindowEntries(data,config); if(!entries.length){ contextWindowGuide.innerHTML='<div class="alert info"><strong>Context window guide</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u8FD8\u6CA1\u6709\u53EF\u89E3\u6790\u7684 Models\u3002</div></div>'; return; } const defaultRef=String(router.default || '').trim(); const longRef=String(router.longContext || '').trim(); const defaultEntry=entries.find(item=>item.id===defaultRef); const longEntry=entries.find(item=>item.id===longRef); const ranked=entries.filter(item=>item.contextWindowTokens).sort((a,b)=>(b.contextWindowTokens || 0)-(a.contextWindowTokens || 0)); const best=ranked[0]; const missingCount=entries.filter(item=>!item.contextWindowTokens || !item.safeInputTokens).length; const messages=[]; let level='info'; if(missingCount){ level='warn'; messages.push('\u6709 '+missingCount+' \u4E2A\u6A21\u578B\u7F3A\u5C11 context_window_tokens \u6216 safe_input_tokens\uFF0C\u8D85\u5927\u8BF7\u6C42\u53EF\u80FD\u65E0\u6CD5\u63D0\u524D\u964D\u7EA7/\u5207\u6362\u3002'); } if(entries.length>1 && !longRef){ level='warn'; messages.push('\u591A\u6A21\u578B\u914D\u7F6E\u672A\u8BBE\u7F6E Router.longContext\uFF0C\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u4F1A\u7EE7\u7EED\u4F7F\u7528\u5DF2\u9009\u6A21\u578B\u3002'); } if(longRef && !longEntry){ level='warn'; messages.push('Router.longContext \u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id\u3002'); } if(longEntry && (!longEntry.contextWindowTokens || !longEntry.safeInputTokens)){ level='warn'; messages.push('Router.longContext \u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u6216\u5B89\u5168\u8F93\u5165\u5143\u6570\u636E\u3002'); } if(defaultEntry?.contextWindowTokens && longEntry?.contextWindowTokens && longEntry.contextWindowTokens <= defaultEntry.contextWindowTokens){ level='warn'; messages.push('Router.longContext \u7684\u7A97\u53E3\u4E0D\u9AD8\u4E8E Router.default\uFF0C\u53EF\u80FD\u65E0\u6CD5\u63D0\u5347\u5927\u4E0A\u4E0B\u6587\u4F53\u9A8C\u3002'); } if(!messages.length){ messages.push('\u5F53\u524D\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E\u548C Router.longContext \u914D\u7F6E\u53EF\u7528\u4E8E\u5927\u4E0A\u4E0B\u6587 fallback\u3002'); } const canApplyBest=best?.id && best.id!==longRef && (!defaultEntry?.contextWindowTokens || (best.contextWindowTokens || 0)>defaultEntry.contextWindowTokens); const summaryRows=[['Default', defaultRef || '-'],['Default ctx', defaultEntry?.contextWindowTokens || '?'],['Long context', longRef || '-'],['Long ctx', longEntry?.contextWindowTokens || '?'],['Largest ctx', best ? (best.id+' / '+best.contextWindowTokens) : '-'],['Missing metadata', missingCount]]; contextWindowGuide.innerHTML='<div class="alert '+level+'"><div class="row"><strong>Context window guide</strong>'+(best ? '<span class="pill">largest '+esc(best.id)+'</span>' : '')+'</div><div class="diff-summary">'+summaryRows.map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('')+'</div><ul>'+messages.map(message=>'<li>'+esc(message)+'</li>').join('')+'</ul>'+(canApplyBest ? '<div class="row" style="margin-top:.5rem"><button type="button" data-context-action="set-long-context" data-model-id="'+esc(best.id)+'">\u8BBE\u4E3A Router.longContext</button><span class="muted">'+esc(best.modelName || '')+'</span></div>' : '')+'</div>';}function getSmartCandidateGuideEntries(data,config){ const modelMap=data?.modelMap || {}; const draftModels=Array.isArray(config?.Models) ? config.Models : []; if(draftModels.length){ return draftModels.map(model=>{ const id=String(model?.id || '').trim(); const compiled=id ? modelMap[id] : null; const caps=compiled?.capabilities || {}; return { id, modelName:model?.model || compiled?.modelName || '-', contextWindowTokens:readModelMetadataNumber(model,'context_window_tokens') || caps.contextWindowTokens || 0, thinkingSupported:caps.thinking?.supported !== false }; }).filter(item=>item.id); } return Object.entries(modelMap).map(([id,model])=>({ id, modelName:model?.modelName || '-', contextWindowTokens:model?.capabilities?.contextWindowTokens || 0, thinkingSupported:model?.capabilities?.thinking?.supported !== false }));}function pickSmartCandidate(entries,role){ const list=[...entries]; const score=(item)=>{ const text=(item.id+' '+item.modelName).toLowerCase(); let value=0; if(role==='fast'){ if(/haiku|mini|flash|fast|lite|small/.test(text)){ value+=80; } value+=Math.max(0,50-Math.log10((item.contextWindowTokens || 1))*10); } else if(role==='deep'){ if(/opus|reasoner|thinking|o1|o3|gpt-5|sonnet/.test(text)){ value+=80; } if(item.thinkingSupported){ value+=20; } value+=Math.log10((item.contextWindowTokens || 1))*5; } else if(role==='long_context'){ value+=item.contextWindowTokens || 0; } else { if(/sonnet|gpt-4|gpt-5|default|balanced/.test(text)){ value+=80; } value+=Math.log10((item.contextWindowTokens || 1))*8; } return value; }; return list.sort((a,b)=>score(b)-score(a) || a.id.localeCompare(b.id))[0];}function renderSmartCandidateGuide(data,summary){ const config=data?.normalizedConfig || currentDraftConfig || {}; const entries=getSmartCandidateGuideEntries(data,config); const candidates=Array.isArray(summary?.candidates) ? summary.candidates : []; const configured=new Set(candidates.map(candidate=>candidate.model?.ref).filter(Boolean)); if(!entries.length){ smartCandidateGuide.innerHTML='<div class="alert info"><strong>Candidate guide</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u8FD8\u6CA1\u6709\u53EF\u89E3\u6790\u7684 Models\uFF0C\u5148\u6DFB\u52A0\u6A21\u578B\u540E\u518D\u914D\u7F6E\u5019\u9009\u3002</div></div>'; return; } const roles=[ { key:'fast', label:'Fast', description:'\u9AD8\u9891\u8F7B\u91CF\u4EFB\u52A1\u3001\u540E\u53F0\u8BF7\u6C42\u548C\u4F4E\u5EF6\u8FDF\u5019\u9009' }, { key:'balanced', label:'Balanced', description:'\u9ED8\u8BA4\u65E5\u5E38\u7F16\u7801\u3001\u89E3\u91CA\u548C\u4E2D\u7B49\u590D\u6742\u5EA6\u4EFB\u52A1' }, { key:'deep', label:'Deep', description:'\u590D\u6742\u63A8\u7406\u3001\u67B6\u6784\u8BBE\u8BA1\u548C\u9700\u8981\u66F4\u5F3A\u6A21\u578B\u7684\u4EFB\u52A1' }, { key:'long_context', label:'Long context', description:'\u5927\u4E0A\u4E0B\u6587\u3001\u957F\u6587\u4EF6\u548C\u8D85\u957F\u4F1A\u8BDD\u515C\u5E95\u5019\u9009' }, ]; const rows=roles.map(role=>{ const picked=pickSmartCandidate(entries,role.key); const configuredRole=picked && configured.has(picked.id); const cls=configuredRole ? 'info' : 'warn'; const button=(!configuredRole && picked) ? '<button type="button" data-add-smart-candidate-suggestion="'+esc(picked.id)+'" data-description="'+esc(role.key+' candidate')+'">Add candidate</button>' : ''; return '<div class="alert '+cls+'"><div class="row"><strong>'+esc(role.label)+'</strong><span class="pill '+cls+'">'+esc(configuredRole ? 'configured' : 'suggested')+'</span></div><div>'+esc(role.description)+'</div><div class="muted">'+(picked ? ('<code>'+esc(picked.id)+'</code> \xB7 '+esc(picked.modelName || '-')+' \xB7 ctx '+esc(picked.contextWindowTokens || '?')) : 'no model suggestion')+'</div>'+(button ? '<div class="row" style="margin-top:.5rem">'+button+'</div>' : '')+'</div>'; }); smartCandidateGuide.innerHTML='<div class="alert info"><strong>Candidate guide</strong><div class="muted">\u5EFA\u8BAE\u81F3\u5C11\u8986\u76D6 fast / balanced / deep\uFF0C\u9700\u8981\u5927\u4E0A\u4E0B\u6587\u65F6\u518D\u52A0\u5165 long-context \u5019\u9009\u3002</div></div>'+rows.join('');}function renderSmartRouterExplanation(data){ const summary=data?.smartRouterExplanation || {}; const rules=Array.isArray(summary.rules) ? summary.rules : []; const candidates=Array.isArray(summary.candidates) ? summary.candidates : []; const warnings=Array.isArray(summary.warnings) ? summary.warnings : []; const refLabel=(model)=>model?.ref ? ('<code>'+esc(model.ref)+'</code><div class="muted">'+esc(model.target?.providerName || model.status || '-')+' / '+esc(model.target?.modelName || '-')+'</div>') : '<span class="muted">-</span>'; const switchRows=[['Enabled', summary.enabled ? 'true' : 'false'],['Rules', rules.length],['Candidates', candidates.length],['Router model', summary.routerModel?.ref || '-'],['Semantic', (summary.semantic?.enabled ? 'on' : 'off')+' / '+(summary.semantic?.mode || '-')],['Sticky', summary.sticky?.enabled ? 'on' : 'off'],['Alignment', summary.sticky?.alignment?.enabled ? 'on' : 'off'],['Fallback', summary.fallback || 'default'],['Warnings', warnings.length]]; smartRouterExplanationSummary.innerHTML=switchRows.map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const order=Array.isArray(summary.routeOrder) ? summary.routeOrder : []; smartRouterRouteOrder.innerHTML='<div class="alert '+(warnings.length ? 'warn' : 'info')+'"><strong>Route order</strong><ol>'+order.map(item=>'<li>'+esc(item)+'</li>').join('')+'</ol>'+(warnings.length ? '<ul>'+warnings.map(item=>'<li>'+esc(item)+'</li>').join('')+'</ul>' : '<div class="muted">SmartRouter \u914D\u7F6E\u5F15\u7528\u5DF2\u89E3\u6790\u3002</div>')+'</div>'; smartRouterRulesTableBody.innerHTML=rules.length ? rules.map(rule=>{ const patternText=(rule.patterns || []).map(pattern=>pattern.type==='exact' ? ('exact: '+(pattern.keywords || []).join(', ')) : ('regex: '+(pattern.pattern || '-'))).join('; '); const statusClass=rule.model?.status === 'resolved' ? 'info' : (rule.model?.status === 'legacy' ? 'warn' : 'critical'); return '<tr>' + '<td>'+esc(rule.order || '-')+'<div class="muted">priority '+esc(rule.priority ?? 0)+'</div></td>' + '<td><strong>'+esc(rule.name || '-')+'</strong><div class="muted">'+esc(rule.description || '-')+'</div><span class="pill '+(rule.enabled ? 'info' : 'warn')+'">'+esc(rule.enabled ? 'enabled' : 'disabled')+'</span></td>' + '<td>'+refLabel(rule.model)+'<span class="pill '+statusClass+'">'+esc(rule.model?.status || '-')+'</span></td>' + '<td>'+esc(patternText || '-')+'</td>' + '<td>'+esc(rule.semantic?.enabled ? 'on' : 'off')+'<div class="muted">'+esc(rule.semantic?.prototype || '-')+'</div></td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No SmartRouter rules configured</td></tr>'; smartRouterCandidatesTableBody.innerHTML=candidates.length ? candidates.map(candidate=>{ const statusClass=candidate.model?.status === 'resolved' ? 'info' : (candidate.model?.status === 'legacy' ? 'warn' : 'critical'); return '<tr><td>'+esc(candidate.order || '-')+'</td><td>'+refLabel(candidate.model)+'</td><td>'+esc(candidate.description || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(candidate.model?.status || '-')+'</span></td></tr>'; }).join('') : '<tr><td colspan="4" class="muted">No SmartRouter candidates configured</td></tr>'; renderSmartCandidateGuide(data,summary);}function renderCompiledModels(data){ lastCompiledModelsData=data || null; const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); const modelPoolEntries=Object.entries(data.modelPools || {}); const modelPoolEndpointCount=modelPoolEntries.reduce((sum,[_modelId,pool])=>sum+((pool.endpoints || []).length),0); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); renderRouterSlotExplanation(data); renderContextWindowGuide(data); renderSmartRouterExplanation(data); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04 / '+modelPoolEntries.length+' \u4E2A model pool / '+modelPoolEndpointCount+' \u4E2A pool endpoint'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; compiledModelPoolsTableBody.innerHTML=modelPoolEntries.length ? modelPoolEntries.map(([modelId,pool])=>{ const endpoints=pool.endpoints || []; return '<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td>'+esc(pool.strategy || '-')+'</td>' + '<td><code>'+esc(pool.activeEndpointId || '-')+'</code></td>' + '<td>'+endpoints.map(endpoint=>{ const latency=endpoint.health?.latency; return '<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+' / '+esc(endpoint.health?.status || 'healthy')+(latency ? ' / avg '+esc(Math.round(latency.averageMs))+'ms' : '')+'</span><div class="muted">'+esc(endpoint.upstreamServiceId || endpoint.api || '-')+'</div></div>'; }).join('')+'</td>' + '<td>'+((pool.warnings || []).length ? pool.warnings.map(w=>'<div class="warning-text">'+esc(w)+'</div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No compiled model pools</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(currentDraftConfig)); renderContextWindowGuide(withDraftCompiledData(currentDraftConfig)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderContextWindowGuide(lastCompiledModelsData); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderContextWindowGuide(withDraftCompiledData(payload)); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [serviceRes,remoteRes]=await Promise.all([fetch('/api/service-info'),fetch('/api/remote-status')]); const data=await serviceRes.json(); const remoteData=await remoteRes.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; serviceModeStatus.textContent=data.runtimeMode || '-'; serviceRoleStatus.textContent=data.serviceRole || '-'; renderRoleConnectionGuide(data); const auth=data.auth || {}; const managed=auth.managedKeys || {}; const quota=auth.quota || {}; const quotaText=Number.isFinite(quota.requestsUsed) ? (' \xB7 quota '+quota.requestsUsed+' req'+(quota.windowResetAt ? ' \xB7 reset '+String(quota.windowResetAt).replace('T',' ').replace('.000Z','Z') : '')) : ''; authStatusSummary.textContent=auth.required ? ((auth.bootstrapConfigured ? 'bootstrap' : 'managed')+' \xB7 '+(managed.active ?? 0)+' active'+quotaText) : 'not configured'; renderAuthQuotaTable(quota); const security=data.security || {}; const issues=Array.isArray(security.issues) ? security.issues : []; securityStatusSummary.textContent=security.status || '-'; securitySummary.className='alert '+((security.status === 'critical') ? 'critical' : (security.status === 'warning' ? 'warn' : 'info')); securitySummary.innerHTML='<strong>Security: '+esc(security.status || '-')+'</strong><div>'+esc(issues[0]?.message || '\u5F53\u524D\u670D\u52A1\u672A\u53D1\u73B0\u660E\u663E\u9274\u6743\u66B4\u9732\u98CE\u9669')+'</div>'+ (issues.length ? '<ul class="mini-list">'+issues.map(issue=>'<li>'+esc(issue.action || issue.code)+'</li>').join('')+'</ul>' : ''); const registration=data.registration || {}; registrationStatusSummary.textContent=registration.enabled ? ((registration.models ?? 0)+' models / '+(registration.upstreamServices ?? 0)+' upstream') : 'disabled'; const remote=remoteData.remote || {}; remoteStatusSummary.textContent=remote.enabled ? ((remote.ready ? 'ready' : (remote.reachable ? 'reachable' : 'unreachable'))+' \xB7 '+(remote.baseUrl || '-')) : 'disabled'; const remoteRegistration=remoteData.remoteRegistration || {}; const remoteRegistrationSummary=remoteRegistration.summary || {}; remoteRegistrationStatusSummary.textContent=remoteRegistration.enabled ? (remoteRegistration.available ? (remoteRegistration.registrationEnabled ? ((remoteRegistrationSummary.models ?? 0)+' remote models / '+(remoteRegistrationSummary.upstreamServices ?? 0)+' upstream') : 'remote registration disabled') : ('unavailable \xB7 '+(remoteRegistration.error || remoteRegistration.baseUrl || '-'))) : 'disabled'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } try { await loadModelPoolHealth(); } catch (_poolError) { modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderContextWindowGuide(lastCompiledModelsData); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addSmartCandidateSuggestion(modelId,description){ const id=String(modelId || '').trim(); if(!id){ return; } const next=extractSmartCandidatesFromForm(); if(!next.some(item=>item.model===id)){ next.push({ model:id, description:description || 'guided candidate' }); } renderSmartCandidatesList(next); syncDraftEditorFromForm(); renderSmartCandidateGuide(withDraftCompiledData(currentDraftConfig), { candidates: next.map((item,index)=>({ order:index+1, description:item.description, model:{ ref:item.model, status:'resolved' } })) }); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });smartCandidateGuide.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-add-smart-candidate-suggestion]'); if(!btn){ return; } addSmartCandidateSuggestion(btn.dataset.addSmartCandidateSuggestion, btn.dataset.description); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });contextWindowGuide.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-context-action]'); if(!btn){ return; } applyContextWindowAction(btn.dataset.contextAction, btn.dataset.modelId); });healthSummary.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-health-action]'); if(btn){ applyHealthAction(btn.dataset.healthAction); } });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics,health,outcome){ metricsGrid.innerHTML=[ ['Health', health?.status || 'idle'], ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Model switch rate', pct(outcome?.modelSwitchRate)], ['Alignment on switch', pct(outcome?.alignmentOnSwitchRate)], ['Context fallback', pct(outcome?.contextWindowFallbackRate)], ['Context exceeded', pct(outcome?.contextWindowExceededRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderOutcomeGroups(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code><span class="muted"> \xB7 '+esc(item.totalTraces)+' traces</span></span><strong>switch '+esc(pct(item.modelSwitchRate))+' \xB7 align '+esc(pct(item.alignmentOnSwitchRate))+' \xB7 cascade '+esc(pct(item.cascadeAfterSwitchRate))+' \xB7 '+esc(fmt(item.averageLatencyMs))+' ms</strong></li>').join('');}function renderRoutingTuning(items){ if(!items || !items.length){ routingTuningList.innerHTML='<li><span class="muted">No routing tuning recommendations</span><strong>healthy</strong></li>'; return; } routingTuningList.innerHTML=items.map(item=>{ const suggestions=Array.isArray(item.configSuggestions) ? item.configSuggestions : []; const suggestionHtml=suggestions.length ? '<div class="muted">config: '+suggestions.map(s=>'<code>'+esc(s.path || '-')+'</code>'+(s.suggestedValue !== undefined ? ' = '+esc(s.suggestedValue) : '')+' \u2014 '+esc(s.reason || '')).join('<br>')+'</div>' : ''; return '<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.code || '-')+'</strong><div class="muted">'+esc(item.message || '')+'</div><div class="muted">'+esc(item.evidence || '')+'</div>'+suggestionHtml+'</span><strong>'+esc(item.action || '')+'</strong></li>'; }).join('');}function renderQualityEvidence(summary){ const items=summary?.samples || []; qualityEvidenceSummary.innerHTML=[['Samples',summary?.totalSamples || 0],['Risk',summary?.failureSamples || 0],['Improvement',summary?.improvementSamples || 0],['Speed risk',summary?.speedRiskSamples || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ qualityEvidenceList.innerHTML='<li><span class="muted">No quality evidence samples</span><strong>0</strong></li>'; return; } qualityEvidenceList.innerHTML=items.map(item=>'<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.type || '-')+'</strong><div class="muted">'+esc(item.requestId || '')+' \xB7 '+esc((item.routeReason || []).join(' / '))+'</div><div class="muted">'+esc(item.evidence || '')+'</div></span><strong>'+esc(item.action || '')+'</strong></li>').join('');}function renderTaskComparison(summary){ const items=summary?.comparisons || []; taskComparisonSummary.innerHTML=[['Tasks',summary?.totalComparedTasks || 0],['Traces',summary?.totalComparedTraces || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ taskComparisonList.innerHTML='<li><span class="muted">No comparable task samples</span><strong>0</strong></li>'; return; } taskComparisonList.innerHTML=items.map(item=>'<li><span><strong>'+esc(item.taskKey || '-')+'</strong><div class="muted">best '+esc(item.bestModel || '-')+' \xB7 baseline '+esc(item.baselineModel || '-')+' \xB7 fastest '+esc(item.fastestModel || '-')+'</div><div class="muted">failure lift '+esc(pct(item.failureRateDelta || 0))+' \xB7 latency lift '+esc(fmt(item.latencyDeltaMs || 0))+' ms \xB7 models '+esc(item.modelCount || 0)+'</div></span><strong>'+esc(item.totalTraces || 0)+' traces</strong></li>').join('');}function renderBenchmarkSummary(taskComparison,qualityEvidence){ const bestQuality=taskComparison?.bestQualityLiftTask; const bestSpeed=taskComparison?.bestSpeedLiftTask; benchmarkSummary.innerHTML=[ ['Comparable tasks',taskComparison?.totalComparedTasks || 0], ['Evidence samples',qualityEvidence?.totalSamples || 0], ['Best quality lift',bestQuality ? pct(bestQuality.failureRateDelta || 0) : '-'], ['Best speed lift',bestSpeed ? (fmt(bestSpeed.latencyDeltaMs || 0)+' ms') : '-'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const actions=[]; if((taskComparison?.totalComparedTasks || 0)===0){ actions.push(['Collect comparable traces','Send the same task class through at least two final models, then refresh metrics.']); } if((qualityEvidence?.totalSamples || 0)===0){ actions.push(['Collect quality evidence','Enable cascade, shadow, context-window or model-pool signals so routing wins and risks become visible.']); } actions.push(['Run fixed benchmark','ctr eval --tasks && ctr eval --run --models "sonnet;haiku" --json']); actions.push(['Add calibration','Attach humanScore or judgeScore to ctr eval input results before treating rubric scores as release evidence.']); benchmarkActionList.innerHTML=actions.map(([title,detail])=>'<li><span><strong>'+esc(title)+'</strong><div class="muted">'+esc(detail)+'</div></span><strong>benchmark</strong></li>').join('');}function renderRouteDecisionSummaries(items){ const decisions=Array.isArray(items) ? items.slice(0,5) : []; if(!decisions.length){ routeDecisionSummaryList.innerHTML='<li><span class="muted">No recent route decisions</span><strong>0</strong></li>'; return; } routeDecisionSummaryList.innerHTML=decisions.map(item=>{ const meta=[item.sourceLabel || item.source || '-', item.ruleName ? ('rule '+item.ruleName) : '', item.semanticIntent ? ('intent '+item.semanticIntent) : '', item.confidenceLabel || '', item.latencyMs !== undefined ? (fmt(item.latencyMs)+' ms') : ''].filter(Boolean).join(' \xB7 '); const fallback=item.fallbackReason ? '<div class="muted">fallback: '+esc(item.fallbackReason)+'</div>' : ''; return '<li><span><strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(meta)+'</div>'+fallback+'</span><button type="button" data-request="'+esc(item.requestId || '')+'">View</button></li>'; }).join('');}function renderSwitchContinuitySummaries(items){ const summaries=Array.isArray(items) ? items.slice(0,5) : []; if(!summaries.length){ switchContinuitySummaryList.innerHTML='<li><span class="muted">No recent switch continuity</span><strong>0</strong></li>'; return; } switchContinuitySummaryList.innerHTML=summaries.map(item=>{ const cls=item.status === 'critical' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const meta=[item.transition || (item.finalModel || '-'), item.sourceLabel || item.source || '-', item.alignmentUsed ? 'aligned' : '', item.cascadeTriggered ? 'cascade' : '', item.latencyMs !== undefined ? (fmt(item.latencyMs)+' ms') : ''].filter(Boolean).join(' \xB7 '); const action=item.action ? '<div class="muted">'+esc(item.action)+'</div>' : ''; return '<li><span><span class="pill '+esc(cls)+'">'+esc(item.status || 'unknown')+'</span> <strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(meta)+'</div>'+action+'</span><button type="button" data-request="'+esc(item.requestId || '')+'">View</button></li>'; }).join('');}function renderAnomalies(anomalies,health){ const status=health?.status || 'idle'; const message=health?.message || 'No governance traces yet.'; const actions=Array.isArray(health?.actions) ? health.actions : []; healthSummary.className='alert '+esc(status === 'critical' ? 'critical' : (status === 'watch' ? 'warn' : 'info')); healthSummary.innerHTML='<strong>Health: '+esc(status)+'</strong><div>'+esc(message)+'</div>'+ (actions.length ? '<ul class="mini-list">'+actions.map(action=>'<li><button type="button" data-health-action="'+esc(action)+'">'+esc(action)+'</button></li>').join('')+'</ul>' : ''); if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function applyHealthAction(action){ const text=String(action || '').toLowerCase(); const routeReasonInput=document.getElementById('routeReason'); const cascadeSelect=document.getElementById('cascadeTriggered'); const shadowSelect=document.getElementById('shadowChecked'); if(text.includes('cascade')){ cascadeSelect.value='true'; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered cascade traces'; } else if(text.includes('shadow')){ shadowSelect.value='true'; cascadeSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered shadow traces'; } else { cascadeSelect.value=''; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: showing recent traces'; } loadTraces(); document.getElementById('traceTable').scrollIntoView({ behavior:'smooth', block:'start' });}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes,healthRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query), fetch('/api/governance/health'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); const healthData=await healthRes.json(); const health=healthData.health || metricsData.health; renderMetrics(metricsData.metrics || {},health,metricsData.outcome || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRoutingTuning(health?.routingTuning || []); renderQualityEvidence(metricsData.qualityEvidence || {}); renderTaskComparison(metricsData.taskComparison || {}); renderBenchmarkSummary(metricsData.taskComparison || {},metricsData.qualityEvidence || {}); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderOutcomeGroups(routeOutcomeRanking,metricsData.outcome?.byRouteReason || [],'No route outcomes'); renderOutcomeGroups(modelOutcomeRanking,metricsData.outcome?.byFinalModel || [],'No model outcomes'); renderOutcomeGroups(intentOutcomeRanking,metricsData.outcome?.bySemanticIntent || [],'No intent outcomes'); renderTrendTable(metricsData || {}); const traces=data.traces || []; renderRouteDecisionSummaries(data.routeDecisions || traces.map(t=>t.decisionSummary).filter(Boolean)); renderSwitchContinuitySummaries(data.switchContinuity || traces.map(t=>t.switchSummary).filter(Boolean)); if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);routeDecisionSummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });switchContinuitySummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
|
|
5356
5620
|
}
|
|
5357
5621
|
var init_workbench = __esm({
|
|
5358
5622
|
"src/ui/workbench.ts"() {
|
|
@@ -5704,6 +5968,145 @@ function analyzeModelReferenceImpact(config, nextCompiled) {
|
|
|
5704
5968
|
}
|
|
5705
5969
|
};
|
|
5706
5970
|
}
|
|
5971
|
+
function summarizeSmartRouterExplanation(normalized, compiled) {
|
|
5972
|
+
const smartRouter = deriveRuntimeSmartRouterConfig(normalized, normalized) ?? {};
|
|
5973
|
+
const modelMap = compiled.modelMap ?? {};
|
|
5974
|
+
const resolveLegacyRef = (value) => {
|
|
5975
|
+
const separatorIndex = value.indexOf(",");
|
|
5976
|
+
if (separatorIndex < 0) {
|
|
5977
|
+
return null;
|
|
5978
|
+
}
|
|
5979
|
+
const providerName = value.slice(0, separatorIndex).trim();
|
|
5980
|
+
const modelName = value.slice(separatorIndex + 1).trim();
|
|
5981
|
+
const provider = compiled.providers.find((item) => item.name === providerName);
|
|
5982
|
+
if (!provider || !provider.models.includes(modelName)) {
|
|
5983
|
+
return null;
|
|
5984
|
+
}
|
|
5985
|
+
return {
|
|
5986
|
+
providerName,
|
|
5987
|
+
modelName,
|
|
5988
|
+
protocol: provider.transformer?.use?.[0]
|
|
5989
|
+
};
|
|
5990
|
+
};
|
|
5991
|
+
const resolveRef = (ref) => {
|
|
5992
|
+
const value = typeof ref === "string" ? ref.trim() : "";
|
|
5993
|
+
const resolved = value ? modelMap[value] : void 0;
|
|
5994
|
+
const legacyTarget = !resolved && value ? resolveLegacyRef(value) : null;
|
|
5995
|
+
return {
|
|
5996
|
+
ref: value,
|
|
5997
|
+
status: !value ? "empty" : resolved || legacyTarget ? "resolved" : value.includes(",") ? "legacy" : "missing",
|
|
5998
|
+
target: resolved ? {
|
|
5999
|
+
providerName: resolved.providerName,
|
|
6000
|
+
modelName: resolved.modelName,
|
|
6001
|
+
protocol: resolved.protocol
|
|
6002
|
+
} : legacyTarget ? {
|
|
6003
|
+
providerName: legacyTarget.providerName,
|
|
6004
|
+
modelName: legacyTarget.modelName,
|
|
6005
|
+
protocol: legacyTarget.protocol
|
|
6006
|
+
} : null
|
|
6007
|
+
};
|
|
6008
|
+
};
|
|
6009
|
+
const rules = [...smartRouter.rules ?? []].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)).map((rule, index) => {
|
|
6010
|
+
const patterns = Array.isArray(rule.patterns) ? rule.patterns : [];
|
|
6011
|
+
return {
|
|
6012
|
+
order: index + 1,
|
|
6013
|
+
name: rule.name ?? "",
|
|
6014
|
+
enabled: rule.enabled !== false,
|
|
6015
|
+
priority: rule.priority ?? 0,
|
|
6016
|
+
description: rule.description ?? "",
|
|
6017
|
+
model: resolveRef(rule.model),
|
|
6018
|
+
patternCount: patterns.length,
|
|
6019
|
+
patterns: patterns.map((pattern) => ({
|
|
6020
|
+
type: pattern.type ?? "",
|
|
6021
|
+
keywords: Array.isArray(pattern.keywords) ? pattern.keywords : [],
|
|
6022
|
+
pattern: pattern.pattern ?? ""
|
|
6023
|
+
})),
|
|
6024
|
+
semantic: {
|
|
6025
|
+
enabled: rule.semantic_profile?.enabled !== false,
|
|
6026
|
+
prototype: rule.semantic_profile?.prototype ?? rule.description ?? "",
|
|
6027
|
+
threshold: rule.semantic_profile?.threshold
|
|
6028
|
+
}
|
|
6029
|
+
};
|
|
6030
|
+
});
|
|
6031
|
+
const candidates = (smartRouter.candidates ?? []).map((candidate, index) => ({
|
|
6032
|
+
order: index + 1,
|
|
6033
|
+
model: resolveRef(candidate.model),
|
|
6034
|
+
description: candidate.description ?? ""
|
|
6035
|
+
}));
|
|
6036
|
+
const routerModel = resolveRef(smartRouter.router_model);
|
|
6037
|
+
const alignmentSummarizer = resolveRef(smartRouter.sticky?.alignment?.summarizer_model);
|
|
6038
|
+
const classifierModel = resolveRef(smartRouter.semantic?.classifier_model);
|
|
6039
|
+
const warnings = [];
|
|
6040
|
+
const isUnresolvedRef = (modelRef) => modelRef.status === "missing" || modelRef.status === "legacy";
|
|
6041
|
+
const unresolvedRefWarning = (label, modelRef) => `${label} "${modelRef.ref}" does not resolve to a compiled Models[].id or provider,model reference.`;
|
|
6042
|
+
if (smartRouter.enabled && rules.length === 0) {
|
|
6043
|
+
warnings.push("SmartRouter enabled but no explicit rules are configured.");
|
|
6044
|
+
}
|
|
6045
|
+
if (smartRouter.enabled && smartRouter.router_model && candidates.length < 2) {
|
|
6046
|
+
warnings.push("router_model is configured but fewer than 2 candidates are available.");
|
|
6047
|
+
}
|
|
6048
|
+
if (isUnresolvedRef(routerModel)) {
|
|
6049
|
+
warnings.push(unresolvedRefWarning("SmartRouter.router_model", routerModel));
|
|
6050
|
+
}
|
|
6051
|
+
rules.forEach((rule) => {
|
|
6052
|
+
if (isUnresolvedRef(rule.model)) {
|
|
6053
|
+
warnings.push(unresolvedRefWarning(`SmartRouter rule "${rule.name}" model`, rule.model));
|
|
6054
|
+
}
|
|
6055
|
+
});
|
|
6056
|
+
candidates.forEach((candidate) => {
|
|
6057
|
+
if (isUnresolvedRef(candidate.model)) {
|
|
6058
|
+
warnings.push(unresolvedRefWarning("SmartRouter candidate", candidate.model));
|
|
6059
|
+
}
|
|
6060
|
+
});
|
|
6061
|
+
if (smartRouter.semantic?.enabled && smartRouter.semantic?.mode === "classifier" && classifierModel.status === "empty") {
|
|
6062
|
+
warnings.push("Semantic classifier mode is enabled but classifier_model is empty.");
|
|
6063
|
+
}
|
|
6064
|
+
if (smartRouter.semantic?.enabled && smartRouter.semantic?.mode === "classifier" && isUnresolvedRef(classifierModel)) {
|
|
6065
|
+
warnings.push(unresolvedRefWarning("SmartRouter.semantic.classifier_model", classifierModel));
|
|
6066
|
+
}
|
|
6067
|
+
if (smartRouter.sticky?.alignment?.enabled && alignmentSummarizer.status === "empty") {
|
|
6068
|
+
warnings.push("Sticky alignment is enabled but summarizer_model is empty.");
|
|
6069
|
+
}
|
|
6070
|
+
if (smartRouter.sticky?.alignment?.enabled && isUnresolvedRef(alignmentSummarizer)) {
|
|
6071
|
+
warnings.push(unresolvedRefWarning("SmartRouter.sticky.alignment.summarizer_model", alignmentSummarizer));
|
|
6072
|
+
}
|
|
6073
|
+
return {
|
|
6074
|
+
enabled: Boolean(smartRouter.enabled),
|
|
6075
|
+
analysisScope: smartRouter.analysis_scope ?? "last_message",
|
|
6076
|
+
routeOrder: [
|
|
6077
|
+
"1. explicit rules by priority",
|
|
6078
|
+
"2. semantic match when enabled",
|
|
6079
|
+
"3. router_model candidates when configured",
|
|
6080
|
+
"4. sticky correction for session continuity",
|
|
6081
|
+
`5. fallback ${smartRouter.fallback ?? "default"}`
|
|
6082
|
+
],
|
|
6083
|
+
rules,
|
|
6084
|
+
routerModel,
|
|
6085
|
+
candidates,
|
|
6086
|
+
fallback: smartRouter.fallback ?? "default",
|
|
6087
|
+
cacheTtl: smartRouter.cache_ttl,
|
|
6088
|
+
maxTokens: smartRouter.max_tokens,
|
|
6089
|
+
semantic: {
|
|
6090
|
+
enabled: Boolean(smartRouter.semantic?.enabled),
|
|
6091
|
+
mode: smartRouter.semantic?.mode ?? "embedding",
|
|
6092
|
+
threshold: smartRouter.semantic?.threshold,
|
|
6093
|
+
classifierModel,
|
|
6094
|
+
prototypeCount: Object.keys(smartRouter.semantic?.prototypes ?? {}).length
|
|
6095
|
+
},
|
|
6096
|
+
sticky: {
|
|
6097
|
+
enabled: Boolean(smartRouter.sticky?.enabled),
|
|
6098
|
+
sessionTtlMs: smartRouter.sticky?.session_ttl_ms,
|
|
6099
|
+
fingerprintSimilarityThreshold: smartRouter.sticky?.fingerprint_similarity_threshold,
|
|
6100
|
+
breakOnExplicitRoute: Boolean(smartRouter.sticky?.break_on_explicit_route),
|
|
6101
|
+
alignment: {
|
|
6102
|
+
enabled: Boolean(smartRouter.sticky?.alignment?.enabled),
|
|
6103
|
+
summarizerModel: alignmentSummarizer,
|
|
6104
|
+
maxSummaryTokens: smartRouter.sticky?.alignment?.max_summary_tokens
|
|
6105
|
+
}
|
|
6106
|
+
},
|
|
6107
|
+
warnings
|
|
6108
|
+
};
|
|
6109
|
+
}
|
|
5707
6110
|
function projectConfiguredBranch(raw, normalized) {
|
|
5708
6111
|
if (raw === void 0) {
|
|
5709
6112
|
return void 0;
|
|
@@ -5969,6 +6372,16 @@ var init_server = __esm({
|
|
|
5969
6372
|
createServer = (config) => {
|
|
5970
6373
|
const server = new import_llms.default(config);
|
|
5971
6374
|
const configuredThresholds = config.initialConfig?.Governance?.observability?.anomaly_thresholds ?? {};
|
|
6375
|
+
const readActiveConfig = async () => {
|
|
6376
|
+
try {
|
|
6377
|
+
const currentConfig = await readConfigFile();
|
|
6378
|
+
if (currentConfig && typeof currentConfig === "object" && Object.keys(currentConfig).length > 0) {
|
|
6379
|
+
return currentConfig;
|
|
6380
|
+
}
|
|
6381
|
+
} catch {
|
|
6382
|
+
}
|
|
6383
|
+
return config.initialConfig ?? {};
|
|
6384
|
+
};
|
|
5972
6385
|
const readGovernanceMetricsQuery = (query) => {
|
|
5973
6386
|
const limit = query?.limit ? Number(query.limit) : void 0;
|
|
5974
6387
|
const windowMs = query?.windowMs ? Number(query.windowMs) : void 0;
|
|
@@ -6003,12 +6416,14 @@ var init_server = __esm({
|
|
|
6003
6416
|
return buildDraftConfigView(await readConfigFile());
|
|
6004
6417
|
});
|
|
6005
6418
|
server.app.get("/api/models/compiled", async () => {
|
|
6006
|
-
const normalizedResult = normalizeAndValidateConfig(
|
|
6419
|
+
const normalizedResult = normalizeAndValidateConfig(await readActiveConfig());
|
|
6007
6420
|
const normalized = normalizedResult.config;
|
|
6008
6421
|
const compiled = toCompiledRegistryView(normalized);
|
|
6009
6422
|
const capabilityWarnings = collectCapabilityWarnings(normalized);
|
|
6010
6423
|
return {
|
|
6011
6424
|
...compiled,
|
|
6425
|
+
router: normalized.Router ?? {},
|
|
6426
|
+
smartRouterExplanation: summarizeSmartRouterExplanation(normalized, compiled),
|
|
6012
6427
|
capabilityWarnings,
|
|
6013
6428
|
warnings: normalizedResult.warnings,
|
|
6014
6429
|
issueReport: buildValidationIssueReport({
|
|
@@ -6019,7 +6434,7 @@ var init_server = __esm({
|
|
|
6019
6434
|
};
|
|
6020
6435
|
});
|
|
6021
6436
|
server.app.get("/api/models/pool-health", async () => {
|
|
6022
|
-
return buildModelPoolHealthReport(
|
|
6437
|
+
return buildModelPoolHealthReport(await readActiveConfig());
|
|
6023
6438
|
});
|
|
6024
6439
|
server.app.post("/api/models/compiled/preview", async (req, reply) => {
|
|
6025
6440
|
const rawConfig = req.body ?? {};
|
|
@@ -6047,7 +6462,7 @@ var init_server = __esm({
|
|
|
6047
6462
|
})
|
|
6048
6463
|
};
|
|
6049
6464
|
}
|
|
6050
|
-
const currentCompiled = toCompiledRegistryView(
|
|
6465
|
+
const currentCompiled = toCompiledRegistryView(await readActiveConfig());
|
|
6051
6466
|
const previewCompiled = toCompiledRegistryView(result.config);
|
|
6052
6467
|
const previewCapabilityWarnings = collectCapabilityWarnings(result.config);
|
|
6053
6468
|
return {
|
|
@@ -6055,6 +6470,7 @@ var init_server = __esm({
|
|
|
6055
6470
|
providers: previewCompiled.providers,
|
|
6056
6471
|
modelMap: previewCompiled.modelMap,
|
|
6057
6472
|
modelPools: previewCompiled.modelPools,
|
|
6473
|
+
smartRouterExplanation: summarizeSmartRouterExplanation(result.config, previewCompiled),
|
|
6058
6474
|
normalizedConfig: buildDraftConfigView(result.config),
|
|
6059
6475
|
diff: diffCompiledRegistry(currentCompiled, previewCompiled),
|
|
6060
6476
|
referenceImpact: analyzeModelReferenceImpact(result.config, previewCompiled),
|
|
@@ -6243,15 +6659,18 @@ var init_server = __esm({
|
|
|
6243
6659
|
const limit = req.query?.limit ? Number(req.query.limit) : void 0;
|
|
6244
6660
|
const cascadeTriggered = req.query?.cascadeTriggered === void 0 ? void 0 : String(req.query.cascadeTriggered).toLowerCase() === "true";
|
|
6245
6661
|
const shadowChecked = req.query?.shadowChecked === void 0 ? void 0 : String(req.query.shadowChecked).toLowerCase() === "true";
|
|
6662
|
+
const traces = governanceTraceStore.list({
|
|
6663
|
+
requestId: req.query?.requestId,
|
|
6664
|
+
sessionKey: req.query?.sessionKey,
|
|
6665
|
+
routeReason: req.query?.routeReason,
|
|
6666
|
+
cascadeTriggered,
|
|
6667
|
+
shadowChecked,
|
|
6668
|
+
limit: Number.isFinite(limit) ? limit : void 0
|
|
6669
|
+
});
|
|
6246
6670
|
return {
|
|
6247
|
-
traces
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
routeReason: req.query?.routeReason,
|
|
6251
|
-
cascadeTriggered,
|
|
6252
|
-
shadowChecked,
|
|
6253
|
-
limit: Number.isFinite(limit) ? limit : void 0
|
|
6254
|
-
})
|
|
6671
|
+
traces,
|
|
6672
|
+
routeDecisions: traces.map((trace) => summarizeRouteDecisionTrace(trace)),
|
|
6673
|
+
switchContinuity: traces.map((trace) => summarizeSwitchContinuityTrace(trace))
|
|
6255
6674
|
};
|
|
6256
6675
|
});
|
|
6257
6676
|
server.app.get("/api/governance/metrics", async (req) => {
|
|
@@ -6376,7 +6795,11 @@ var init_server = __esm({
|
|
|
6376
6795
|
message: "Governance trace not found"
|
|
6377
6796
|
};
|
|
6378
6797
|
}
|
|
6379
|
-
return
|
|
6798
|
+
return {
|
|
6799
|
+
...trace,
|
|
6800
|
+
decisionSummary: summarizeRouteDecisionTrace(trace),
|
|
6801
|
+
switchSummary: summarizeSwitchContinuityTrace(trace)
|
|
6802
|
+
};
|
|
6380
6803
|
});
|
|
6381
6804
|
server.app.get("/api/governance/archives", async (req) => {
|
|
6382
6805
|
const limit = req.query?.limit ? Number(req.query.limit) : void 0;
|
|
@@ -8426,6 +8849,13 @@ var init_trigger = __esm({
|
|
|
8426
8849
|
this.apiTimeoutMs
|
|
8427
8850
|
);
|
|
8428
8851
|
if (req.governanceTrace) {
|
|
8852
|
+
req.governanceTrace.routeDecision = {
|
|
8853
|
+
source: result.routeSource ?? (result.matched ? "smart_router" : "no_match"),
|
|
8854
|
+
ruleName: result.rule?.name,
|
|
8855
|
+
confidence: result.confidence,
|
|
8856
|
+
model: result.model,
|
|
8857
|
+
fallbackReason: result.matched ? void 0 : "SmartRouter did not match; request continued to the basic Router fallback path."
|
|
8858
|
+
};
|
|
8429
8859
|
if (result.routeSource === "smart_rule" && result.rule?.name) {
|
|
8430
8860
|
appendTraceReason(req.governanceTrace, `smart_rule:${result.rule.name}`);
|
|
8431
8861
|
} else if (result.routeSource === "semantic_match" && result.rule?.name) {
|
|
@@ -11880,6 +12310,89 @@ async function reportRuntimeServiceContext(config, deps) {
|
|
|
11880
12310
|
deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u63D0\u793A\uFF1A${remoteStatus.error}`);
|
|
11881
12311
|
}
|
|
11882
12312
|
}
|
|
12313
|
+
function getRouterSlotRef(config, key) {
|
|
12314
|
+
const value = config.Router?.[key];
|
|
12315
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
12316
|
+
}
|
|
12317
|
+
function getCompiledModelFromRegistry(registry, ref) {
|
|
12318
|
+
if (!ref) {
|
|
12319
|
+
return void 0;
|
|
12320
|
+
}
|
|
12321
|
+
const direct = registry.modelMap[ref];
|
|
12322
|
+
if (direct) {
|
|
12323
|
+
return direct;
|
|
12324
|
+
}
|
|
12325
|
+
if (!ref.includes(",")) {
|
|
12326
|
+
return void 0;
|
|
12327
|
+
}
|
|
12328
|
+
const [providerName, modelName] = ref.split(",").map((item) => item.trim());
|
|
12329
|
+
return Object.values(registry.modelMap).find(
|
|
12330
|
+
(item) => item.providerName === providerName && item.modelName === modelName
|
|
12331
|
+
);
|
|
12332
|
+
}
|
|
12333
|
+
function formatRouterSlotTarget(compiled) {
|
|
12334
|
+
const upstream = `${compiled.providerName},${compiled.modelName}`;
|
|
12335
|
+
return compiled.id === upstream ? compiled.id : `${compiled.id}\uFF08${upstream}\uFF09`;
|
|
12336
|
+
}
|
|
12337
|
+
function reportRouterSlotSummary(config, registry, deps) {
|
|
12338
|
+
const modelRefCount = Object.keys(registry.modelMap).length;
|
|
12339
|
+
const resolvedSlots = /* @__PURE__ */ new Map();
|
|
12340
|
+
deps.io.info("\u57FA\u7840\u8DEF\u7531\u4F53\u68C0\uFF1A\u68C0\u67E5 Router \u69FD\u4F4D\u662F\u5426\u80FD\u89E3\u6790\u4E3A\u53EF\u7528\u6A21\u578B\u3002");
|
|
12341
|
+
for (const slot of ROUTER_SLOT_DIAGNOSTICS) {
|
|
12342
|
+
const ref = getRouterSlotRef(config, slot.key);
|
|
12343
|
+
if (!ref) {
|
|
12344
|
+
const message = `\u8DEF\u7531\u69FD\u4F4D\uFF1ARouter.${slot.key} \u672A\u914D\u7F6E\uFF08${slot.fallback}\uFF09\u3002`;
|
|
12345
|
+
if (slot.required && modelRefCount > 0) {
|
|
12346
|
+
deps.io.error(message);
|
|
12347
|
+
} else {
|
|
12348
|
+
deps.io.info(message);
|
|
12349
|
+
}
|
|
12350
|
+
continue;
|
|
12351
|
+
}
|
|
12352
|
+
const compiled = getCompiledModelFromRegistry(registry, ref);
|
|
12353
|
+
if (!compiled) {
|
|
12354
|
+
deps.io.error(`\u8DEF\u7531\u69FD\u4F4D\u5F02\u5E38\uFF1ARouter.${slot.key} \u5F15\u7528 "${ref}"\uFF0C\u4F46\u672A\u5728 Models/Providers/Registration \u4E2D\u89E3\u6790\u5230\u53EF\u7528\u6A21\u578B\u3002`);
|
|
12355
|
+
continue;
|
|
12356
|
+
}
|
|
12357
|
+
resolvedSlots.set(slot.key, compiled);
|
|
12358
|
+
deps.io.info(`\u8DEF\u7531\u69FD\u4F4D\uFF1ARouter.${slot.key}\uFF08${slot.label}\uFF09-> ${formatRouterSlotTarget(compiled)}\uFF1B\u89E6\u53D1\uFF1A${slot.trigger}\u3002`);
|
|
12359
|
+
}
|
|
12360
|
+
const thinkingSlot = resolvedSlots.get("think");
|
|
12361
|
+
if (thinkingSlot && thinkingSlot.capabilities.thinking.supported === false) {
|
|
12362
|
+
deps.io.info(
|
|
12363
|
+
`\u601D\u8003\u8DEF\u7531\u63D0\u793A\uFF1ARouter.think \u6307\u5411 ${thinkingSlot.id}\uFF0C\u4F46\u8BE5\u6A21\u578B\u58F0\u660E\u4E0D\u652F\u6301 reasoning\uFF1Bthinking \u8BF7\u6C42\u4F1A\u88AB\u517C\u5BB9\u5C42\u964D\u7EA7\u3002`
|
|
12364
|
+
);
|
|
12365
|
+
}
|
|
12366
|
+
for (const [slotKey, compiled] of resolvedSlots.entries()) {
|
|
12367
|
+
const contextWindowTokens = compiled.capabilities.contextWindowTokens;
|
|
12368
|
+
const safeInputTokens = compiled.capabilities.safeInputTokens;
|
|
12369
|
+
if (!contextWindowTokens) {
|
|
12370
|
+
deps.io.info(
|
|
12371
|
+
`\u4E0A\u4E0B\u6587\u7A97\u53E3\u63D0\u793A\uFF1ARouter.${slotKey} -> ${compiled.id} \u672A\u58F0\u660E metadata.context_window_tokens\uFF1B\u65E0\u6CD5\u786E\u8BA4\u8BE5\u69FD\u4F4D\u7684\u4E0A\u4E0B\u6587\u5BB9\u91CF\u3002`
|
|
12372
|
+
);
|
|
12373
|
+
}
|
|
12374
|
+
if (!safeInputTokens) {
|
|
12375
|
+
deps.io.info(
|
|
12376
|
+
`\u4E0A\u4E0B\u6587\u4FDD\u62A4\u63D0\u793A\uFF1ARouter.${slotKey} -> ${compiled.id} \u672A\u58F0\u660E metadata.safe_input_tokens\uFF1B\u65E0\u6CD5\u63D0\u524D\u628A\u8D85\u5927\u8BF7\u6C42\u5207\u5230\u957F\u4E0A\u4E0B\u6587\u6A21\u578B\u3002`
|
|
12377
|
+
);
|
|
12378
|
+
}
|
|
12379
|
+
}
|
|
12380
|
+
const defaultSlot = resolvedSlots.get("default");
|
|
12381
|
+
const longContextSlot = resolvedSlots.get("longContext");
|
|
12382
|
+
if (!longContextSlot && modelRefCount > 1) {
|
|
12383
|
+
deps.io.info("\u957F\u4E0A\u4E0B\u6587\u63D0\u793A\uFF1A\u672A\u914D\u7F6E Router.longContext\uFF1B\u591A\u6A21\u578B\u914D\u7F6E\u4E0B\uFF0C\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u4E0D\u4F1A\u81EA\u52A8\u5207\u5230\u66F4\u5927\u7A97\u53E3\u6A21\u578B\u3002");
|
|
12384
|
+
return;
|
|
12385
|
+
}
|
|
12386
|
+
if (!longContextSlot) {
|
|
12387
|
+
return;
|
|
12388
|
+
}
|
|
12389
|
+
if (!longContextSlot.capabilities.contextWindowTokens) {
|
|
12390
|
+
deps.io.info("\u957F\u4E0A\u4E0B\u6587\u63D0\u793A\uFF1ARouter.longContext \u672A\u58F0\u660E metadata.context_window_tokens\uFF1Bdoctor \u65E0\u6CD5\u786E\u8BA4\u5B83\u80FD\u627F\u63A5\u5927\u4E0A\u4E0B\u6587 fallback\u3002");
|
|
12391
|
+
}
|
|
12392
|
+
if (defaultSlot?.capabilities.contextWindowTokens && longContextSlot.capabilities.contextWindowTokens && longContextSlot.capabilities.contextWindowTokens <= defaultSlot.capabilities.contextWindowTokens) {
|
|
12393
|
+
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");
|
|
12394
|
+
}
|
|
12395
|
+
}
|
|
11883
12396
|
function createDefaultDeps2(io = createConsoleIO2()) {
|
|
11884
12397
|
return {
|
|
11885
12398
|
readLegacyConfig,
|
|
@@ -11939,6 +12452,7 @@ async function runDoctorCli(customDeps) {
|
|
|
11939
12452
|
}
|
|
11940
12453
|
await reportRuntimeServiceContext(normalized.config, deps);
|
|
11941
12454
|
const registry = buildModelRegistry(normalized.config);
|
|
12455
|
+
reportRouterSlotSummary(normalized.config, registry, deps);
|
|
11942
12456
|
for (const model of normalized.config.Models ?? []) {
|
|
11943
12457
|
const compiledModel = registry.modelMap[model.id];
|
|
11944
12458
|
if (!compiledModel) {
|
|
@@ -12004,7 +12518,7 @@ async function runDoctorCli(customDeps) {
|
|
|
12004
12518
|
deps.io.close?.();
|
|
12005
12519
|
}
|
|
12006
12520
|
}
|
|
12007
|
-
var import_fs9, import_promises7, import_process2, import_child_process2, import_json53, import_js_yaml2;
|
|
12521
|
+
var import_fs9, import_promises7, import_process2, import_child_process2, import_json53, import_js_yaml2, ROUTER_SLOT_DIAGNOSTICS;
|
|
12008
12522
|
var init_doctor = __esm({
|
|
12009
12523
|
"src/doctor/index.ts"() {
|
|
12010
12524
|
"use strict";
|
|
@@ -12026,6 +12540,43 @@ var init_doctor = __esm({
|
|
|
12026
12540
|
init_service_health();
|
|
12027
12541
|
init_templates();
|
|
12028
12542
|
init_api_keys();
|
|
12543
|
+
ROUTER_SLOT_DIAGNOSTICS = [
|
|
12544
|
+
{
|
|
12545
|
+
key: "default",
|
|
12546
|
+
label: "\u9ED8\u8BA4",
|
|
12547
|
+
required: true,
|
|
12548
|
+
fallback: "\u65E0\u9ED8\u8BA4\u6A21\u578B\u65F6\u672C\u5730\u670D\u52A1\u65E0\u6CD5\u7A33\u5B9A\u627F\u63A5\u8BF7\u6C42",
|
|
12549
|
+
trigger: "\u666E\u901A\u8BF7\u6C42\u548C\u5176\u4ED6\u69FD\u4F4D\u672A\u547D\u4E2D\u65F6\u4F7F\u7528"
|
|
12550
|
+
},
|
|
12551
|
+
{
|
|
12552
|
+
key: "think",
|
|
12553
|
+
label: "\u601D\u8003",
|
|
12554
|
+
required: false,
|
|
12555
|
+
fallback: "\u672A\u914D\u7F6E\u65F6 thinking \u8BF7\u6C42\u56DE\u5230 Router.default",
|
|
12556
|
+
trigger: "\u8BF7\u6C42\u5305\u542B thinking \u65F6\u4F7F\u7528"
|
|
12557
|
+
},
|
|
12558
|
+
{
|
|
12559
|
+
key: "longContext",
|
|
12560
|
+
label: "\u957F\u4E0A\u4E0B\u6587",
|
|
12561
|
+
required: false,
|
|
12562
|
+
fallback: "\u672A\u914D\u7F6E\u65F6\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u7EE7\u7EED\u4F7F\u7528\u5DF2\u9009\u6A21\u578B",
|
|
12563
|
+
trigger: "\u8BF7\u6C42 token \u8D85\u8FC7 longContextThreshold \u6216\u5F53\u524D\u6A21\u578B\u5B89\u5168\u8F93\u5165\u7A97\u53E3\u65F6\u4F7F\u7528"
|
|
12564
|
+
},
|
|
12565
|
+
{
|
|
12566
|
+
key: "background",
|
|
12567
|
+
label: "\u540E\u53F0",
|
|
12568
|
+
required: false,
|
|
12569
|
+
fallback: "\u672A\u914D\u7F6E\u65F6\u540E\u53F0/\u8F7B\u91CF\u8BF7\u6C42\u56DE\u5230 Router.default",
|
|
12570
|
+
trigger: "Claude Code \u8F7B\u91CF\u540E\u53F0\u6A21\u578B\u8BF7\u6C42\u65F6\u4F7F\u7528"
|
|
12571
|
+
},
|
|
12572
|
+
{
|
|
12573
|
+
key: "webSearch",
|
|
12574
|
+
label: "\u8054\u7F51\u641C\u7D22",
|
|
12575
|
+
required: false,
|
|
12576
|
+
fallback: "\u672A\u914D\u7F6E\u65F6 web_search \u8BF7\u6C42\u56DE\u5230 Router.default",
|
|
12577
|
+
trigger: "\u8BF7\u6C42\u5305\u542B web_search \u5DE5\u5177\u65F6\u4F7F\u7528"
|
|
12578
|
+
}
|
|
12579
|
+
];
|
|
12029
12580
|
}
|
|
12030
12581
|
});
|
|
12031
12582
|
|
|
@@ -12052,15 +12603,31 @@ function parseOfflineEvaluationInputs(payload) {
|
|
|
12052
12603
|
if (record.error !== void 0 && typeof record.error !== "string") {
|
|
12053
12604
|
throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 error \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
|
|
12054
12605
|
}
|
|
12606
|
+
if (record.judgeError !== void 0 && record.judgeError !== null && typeof record.judgeError !== "string") {
|
|
12607
|
+
throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 judgeError \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
|
|
12608
|
+
}
|
|
12055
12609
|
if (record.latencyMs !== void 0 && (typeof record.latencyMs !== "number" || !Number.isFinite(record.latencyMs) || record.latencyMs < 0)) {
|
|
12056
12610
|
throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 latencyMs \u5FC5\u987B\u662F\u975E\u8D1F\u6570\u5B57\u3002`);
|
|
12057
12611
|
}
|
|
12612
|
+
const humanScore = parseOptionalUnitScore(record.humanScore, `\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 humanScore`);
|
|
12613
|
+
const judgeScore = parseOptionalUnitScore(record.judgeScore, `\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 judgeScore`);
|
|
12614
|
+
if (record.calibrationNotes !== void 0 && record.calibrationNotes !== null && typeof record.calibrationNotes !== "string") {
|
|
12615
|
+
throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 calibrationNotes \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
|
|
12616
|
+
}
|
|
12617
|
+
if (record.judgeFindings !== void 0 && record.judgeFindings !== null && (!Array.isArray(record.judgeFindings) || record.judgeFindings.some((item2) => typeof item2 !== "string"))) {
|
|
12618
|
+
throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 judgeFindings \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\u3002`);
|
|
12619
|
+
}
|
|
12058
12620
|
return {
|
|
12059
12621
|
taskId: record.taskId.trim(),
|
|
12060
12622
|
model: record.model.trim(),
|
|
12061
12623
|
output: record.output,
|
|
12062
12624
|
error: record.error,
|
|
12063
|
-
latencyMs: record.latencyMs
|
|
12625
|
+
latencyMs: record.latencyMs,
|
|
12626
|
+
humanScore,
|
|
12627
|
+
judgeScore,
|
|
12628
|
+
judgeError: typeof record.judgeError === "string" ? record.judgeError : void 0,
|
|
12629
|
+
calibrationNotes: typeof record.calibrationNotes === "string" ? record.calibrationNotes : void 0,
|
|
12630
|
+
judgeFindings: Array.isArray(record.judgeFindings) ? record.judgeFindings : void 0
|
|
12064
12631
|
};
|
|
12065
12632
|
});
|
|
12066
12633
|
}
|
|
@@ -12073,6 +12640,15 @@ function average2(values) {
|
|
|
12073
12640
|
}
|
|
12074
12641
|
return Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(4));
|
|
12075
12642
|
}
|
|
12643
|
+
function parseOptionalUnitScore(value, label) {
|
|
12644
|
+
if (value === void 0 || value === null) {
|
|
12645
|
+
return void 0;
|
|
12646
|
+
}
|
|
12647
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
12648
|
+
throw new Error(`${label} \u5FC5\u987B\u662F 0 \u5230 1 \u4E4B\u95F4\u7684\u6570\u5B57\u3002`);
|
|
12649
|
+
}
|
|
12650
|
+
return Number(value.toFixed(4));
|
|
12651
|
+
}
|
|
12076
12652
|
function rate2(count, total) {
|
|
12077
12653
|
if (!total) {
|
|
12078
12654
|
return 0;
|
|
@@ -12172,6 +12748,66 @@ function extractContentText(content) {
|
|
|
12172
12748
|
return "";
|
|
12173
12749
|
}).filter(Boolean).join("\n");
|
|
12174
12750
|
}
|
|
12751
|
+
function extractFirstJsonObject(text) {
|
|
12752
|
+
const start = text.indexOf("{");
|
|
12753
|
+
if (start === -1) {
|
|
12754
|
+
return void 0;
|
|
12755
|
+
}
|
|
12756
|
+
let depth = 0;
|
|
12757
|
+
let inString = false;
|
|
12758
|
+
let escaped = false;
|
|
12759
|
+
for (let index = start; index < text.length; index += 1) {
|
|
12760
|
+
const char = text[index];
|
|
12761
|
+
if (escaped) {
|
|
12762
|
+
escaped = false;
|
|
12763
|
+
continue;
|
|
12764
|
+
}
|
|
12765
|
+
if (char === "\\") {
|
|
12766
|
+
escaped = true;
|
|
12767
|
+
continue;
|
|
12768
|
+
}
|
|
12769
|
+
if (char === '"') {
|
|
12770
|
+
inString = !inString;
|
|
12771
|
+
continue;
|
|
12772
|
+
}
|
|
12773
|
+
if (inString) {
|
|
12774
|
+
continue;
|
|
12775
|
+
}
|
|
12776
|
+
if (char === "{") {
|
|
12777
|
+
depth += 1;
|
|
12778
|
+
} else if (char === "}") {
|
|
12779
|
+
depth -= 1;
|
|
12780
|
+
if (depth === 0) {
|
|
12781
|
+
try {
|
|
12782
|
+
const payload = JSON.parse(text.slice(start, index + 1));
|
|
12783
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : void 0;
|
|
12784
|
+
} catch {
|
|
12785
|
+
return void 0;
|
|
12786
|
+
}
|
|
12787
|
+
}
|
|
12788
|
+
}
|
|
12789
|
+
}
|
|
12790
|
+
return void 0;
|
|
12791
|
+
}
|
|
12792
|
+
function parseJudgeResult(text) {
|
|
12793
|
+
const payload = extractFirstJsonObject(text);
|
|
12794
|
+
if (!payload) {
|
|
12795
|
+
return void 0;
|
|
12796
|
+
}
|
|
12797
|
+
const rawScore = payload.score ?? payload.judgeScore;
|
|
12798
|
+
const score = typeof rawScore === "number" ? rawScore : typeof rawScore === "string" && rawScore.trim() ? Number(rawScore) : Number.NaN;
|
|
12799
|
+
if (!Number.isFinite(score) || score < 0 || score > 1) {
|
|
12800
|
+
return void 0;
|
|
12801
|
+
}
|
|
12802
|
+
const rawFindings = payload.findings ?? payload.judgeFindings;
|
|
12803
|
+
const judgeFindings = Array.isArray(rawFindings) ? rawFindings.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim()) : typeof rawFindings === "string" && rawFindings.trim() ? [rawFindings.trim()] : void 0;
|
|
12804
|
+
const rawNotes = payload.notes ?? payload.calibrationNotes;
|
|
12805
|
+
return {
|
|
12806
|
+
judgeScore: Number(score.toFixed(4)),
|
|
12807
|
+
calibrationNotes: typeof rawNotes === "string" && rawNotes.trim() ? rawNotes.trim() : void 0,
|
|
12808
|
+
judgeFindings
|
|
12809
|
+
};
|
|
12810
|
+
}
|
|
12175
12811
|
function evaluateDimension(dimension, output3, normalized) {
|
|
12176
12812
|
const findings = [];
|
|
12177
12813
|
const weight = Number.isFinite(dimension.weight) && (dimension.weight ?? 0) > 0 ? dimension.weight : 1;
|
|
@@ -12222,6 +12858,26 @@ function averageDimensionScores(runs) {
|
|
|
12222
12858
|
Object.entries(grouped).map(([id, values]) => [id, average2(values)]).sort(([left], [right]) => left.localeCompare(right))
|
|
12223
12859
|
);
|
|
12224
12860
|
}
|
|
12861
|
+
function buildCalibration(input3, qualityScore) {
|
|
12862
|
+
const scores = [input3.humanScore, input3.judgeScore].filter((value) => typeof value === "number");
|
|
12863
|
+
const findings = [...input3.judgeFindings ?? []];
|
|
12864
|
+
if (!scores.length && !input3.calibrationNotes && !findings.length) {
|
|
12865
|
+
return void 0;
|
|
12866
|
+
}
|
|
12867
|
+
const averageScore = scores.length ? average2(scores) : void 0;
|
|
12868
|
+
const deltaFromQuality = averageScore === void 0 ? void 0 : Number((averageScore - qualityScore).toFixed(4));
|
|
12869
|
+
if (deltaFromQuality !== void 0 && Math.abs(deltaFromQuality) >= 0.25) {
|
|
12870
|
+
findings.push(`calibration_disagreement:${deltaFromQuality}`);
|
|
12871
|
+
}
|
|
12872
|
+
return {
|
|
12873
|
+
humanScore: input3.humanScore,
|
|
12874
|
+
judgeScore: input3.judgeScore,
|
|
12875
|
+
averageScore,
|
|
12876
|
+
deltaFromQuality,
|
|
12877
|
+
notes: input3.calibrationNotes,
|
|
12878
|
+
findings
|
|
12879
|
+
};
|
|
12880
|
+
}
|
|
12225
12881
|
function evaluateRun(task, input3) {
|
|
12226
12882
|
const findings = [];
|
|
12227
12883
|
const output3 = input3.output ?? "";
|
|
@@ -12233,6 +12889,9 @@ function evaluateRun(task, input3) {
|
|
|
12233
12889
|
findings.push(`runner_error:${input3.error}`);
|
|
12234
12890
|
qualityScore = 0;
|
|
12235
12891
|
}
|
|
12892
|
+
if (input3.judgeError) {
|
|
12893
|
+
findings.push(`judge_error:${input3.judgeError}`);
|
|
12894
|
+
}
|
|
12236
12895
|
const dimensions = qualityDimensionsForTask(task);
|
|
12237
12896
|
const dimensionScores = dimensions.map((dimension) => evaluateDimension(dimension, output3, normalized));
|
|
12238
12897
|
for (const dimension of dimensionScores) {
|
|
@@ -12271,6 +12930,12 @@ function evaluateRun(task, input3) {
|
|
|
12271
12930
|
findings.push(`latency_over_budget:${latencyMs}/${task.maxLatencyMs}`);
|
|
12272
12931
|
}
|
|
12273
12932
|
const finalQualityScore = clamp(Math.min(qualityScore, weightedAverageDimensionScore(dimensionScores)));
|
|
12933
|
+
const calibration = buildCalibration(input3, finalQualityScore);
|
|
12934
|
+
if (calibration) {
|
|
12935
|
+
for (const finding of calibration.findings) {
|
|
12936
|
+
findings.push(`calibration:${finding}`);
|
|
12937
|
+
}
|
|
12938
|
+
}
|
|
12274
12939
|
return {
|
|
12275
12940
|
taskId: task.id,
|
|
12276
12941
|
intent: task.intent,
|
|
@@ -12280,6 +12945,7 @@ function evaluateRun(task, input3) {
|
|
|
12280
12945
|
speedScore,
|
|
12281
12946
|
latencyMs,
|
|
12282
12947
|
dimensionScores,
|
|
12948
|
+
calibration,
|
|
12283
12949
|
findings
|
|
12284
12950
|
};
|
|
12285
12951
|
}
|
|
@@ -12327,6 +12993,29 @@ function bestRunForTask(taskId, runs) {
|
|
|
12327
12993
|
return (left.latencyMs ?? Number.POSITIVE_INFINITY) - (right.latencyMs ?? Number.POSITIVE_INFINITY);
|
|
12328
12994
|
})[0];
|
|
12329
12995
|
}
|
|
12996
|
+
function summarizeCalibration(runs) {
|
|
12997
|
+
const calibratedRuns = runs.filter((run2) => run2.calibration);
|
|
12998
|
+
const humanScores = calibratedRuns.map((run2) => run2.calibration?.humanScore).filter((value) => typeof value === "number");
|
|
12999
|
+
const judgeScores = calibratedRuns.map((run2) => run2.calibration?.judgeScore).filter((value) => typeof value === "number");
|
|
13000
|
+
const averageScores = calibratedRuns.map((run2) => run2.calibration?.averageScore).filter((value) => typeof value === "number");
|
|
13001
|
+
const deltas = calibratedRuns.map((run2) => run2.calibration?.deltaFromQuality).filter((value) => typeof value === "number");
|
|
13002
|
+
const highDisagreementRuns = calibratedRuns.filter((run2) => Math.abs(run2.calibration?.deltaFromQuality ?? 0) >= 0.25).map((run2) => ({
|
|
13003
|
+
taskId: run2.taskId,
|
|
13004
|
+
model: run2.model,
|
|
13005
|
+
qualityScore: run2.qualityScore,
|
|
13006
|
+
calibrationScore: run2.calibration?.averageScore ?? 0,
|
|
13007
|
+
deltaFromQuality: run2.calibration?.deltaFromQuality ?? 0,
|
|
13008
|
+
findings: run2.calibration?.findings ?? []
|
|
13009
|
+
}));
|
|
13010
|
+
return {
|
|
13011
|
+
calibratedRuns: calibratedRuns.length,
|
|
13012
|
+
averageHumanScore: average2(humanScores),
|
|
13013
|
+
averageJudgeScore: average2(judgeScores),
|
|
13014
|
+
averageCalibrationScore: average2(averageScores),
|
|
13015
|
+
averageRubricDelta: average2(deltas),
|
|
13016
|
+
highDisagreementRuns
|
|
13017
|
+
};
|
|
13018
|
+
}
|
|
12330
13019
|
function runOfflineTaskEvaluation(inputs, tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
|
|
12331
13020
|
const taskMap = new Map(tasks.map((task) => [task.id, task]));
|
|
12332
13021
|
const missingTaskIds = Array.from(new Set(inputs.map((input3) => input3.taskId).filter((taskId) => !taskMap.has(taskId))));
|
|
@@ -12346,6 +13035,7 @@ function runOfflineTaskEvaluation(inputs, tasks = DEFAULT_OFFLINE_EVALUATION_TAS
|
|
|
12346
13035
|
averageSpeedScore: average2(runs.map((run2) => run2.speedScore)),
|
|
12347
13036
|
averageLatencyMs: latencies.length ? Number(average2(latencies).toFixed(2)) : 0,
|
|
12348
13037
|
averageDimensionScores: averageDimensionScores(runs),
|
|
13038
|
+
calibrationSummary: summarizeCalibration(runs),
|
|
12349
13039
|
bestRunsByTask,
|
|
12350
13040
|
byTask: groupRuns(runs, (run2) => run2.taskId),
|
|
12351
13041
|
byModel: groupRuns(runs, (run2) => run2.model),
|
|
@@ -12405,6 +13095,129 @@ async function runBenchmarkJob(task, model, options) {
|
|
|
12405
13095
|
};
|
|
12406
13096
|
}
|
|
12407
13097
|
}
|
|
13098
|
+
function buildJudgePrompt(task, input3) {
|
|
13099
|
+
return [
|
|
13100
|
+
"You are judging a Claude Trigger Router fixed-task benchmark result.",
|
|
13101
|
+
"Return only compact JSON with this exact shape:",
|
|
13102
|
+
'{"score":0.0,"findings":["short finding"],"notes":"short rationale"}',
|
|
13103
|
+
"Score must be a number from 0 to 1. Do not include markdown.",
|
|
13104
|
+
"",
|
|
13105
|
+
`Task id: ${task.id}`,
|
|
13106
|
+
`Intent: ${task.intent}`,
|
|
13107
|
+
`Expected output: ${task.expectedOutput ?? "A complete answer that satisfies the task prompt."}`,
|
|
13108
|
+
`Prompt: ${task.prompt}`,
|
|
13109
|
+
`Candidate model: ${input3.model}`,
|
|
13110
|
+
"",
|
|
13111
|
+
"Candidate output:",
|
|
13112
|
+
input3.output ?? ""
|
|
13113
|
+
].join("\n");
|
|
13114
|
+
}
|
|
13115
|
+
async function runJudgeJob(task, input3, options) {
|
|
13116
|
+
if (input3.error) {
|
|
13117
|
+
return input3;
|
|
13118
|
+
}
|
|
13119
|
+
if (!task) {
|
|
13120
|
+
return {
|
|
13121
|
+
...input3,
|
|
13122
|
+
judgeError: "unknown_task"
|
|
13123
|
+
};
|
|
13124
|
+
}
|
|
13125
|
+
if (!input3.output?.trim()) {
|
|
13126
|
+
return {
|
|
13127
|
+
...input3,
|
|
13128
|
+
judgeError: "missing_output"
|
|
13129
|
+
};
|
|
13130
|
+
}
|
|
13131
|
+
const url = `${options.baseUrl.replace(/\/+$/, "")}/v1/messages`;
|
|
13132
|
+
try {
|
|
13133
|
+
const response = await options.fetchFn(url, {
|
|
13134
|
+
method: "POST",
|
|
13135
|
+
headers: {
|
|
13136
|
+
"Content-Type": "application/json",
|
|
13137
|
+
"anthropic-version": "2023-06-01",
|
|
13138
|
+
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}`, "x-api-key": options.apiKey } : {}
|
|
13139
|
+
},
|
|
13140
|
+
body: JSON.stringify({
|
|
13141
|
+
model: options.judgeModel,
|
|
13142
|
+
max_tokens: options.maxTokens,
|
|
13143
|
+
stream: false,
|
|
13144
|
+
metadata: {
|
|
13145
|
+
ctr_eval_judge_task_id: task.id,
|
|
13146
|
+
ctr_eval_judge_model: options.judgeModel,
|
|
13147
|
+
ctr_eval_judge_target_model: input3.model
|
|
13148
|
+
},
|
|
13149
|
+
messages: [
|
|
13150
|
+
{
|
|
13151
|
+
role: "user",
|
|
13152
|
+
content: buildJudgePrompt(task, input3)
|
|
13153
|
+
}
|
|
13154
|
+
]
|
|
13155
|
+
}),
|
|
13156
|
+
...options.timeoutMs > 0 ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
|
|
13157
|
+
});
|
|
13158
|
+
if (!response.ok) {
|
|
13159
|
+
return {
|
|
13160
|
+
...input3,
|
|
13161
|
+
judgeError: `http_${response.status}`
|
|
13162
|
+
};
|
|
13163
|
+
}
|
|
13164
|
+
const payload = await response.json();
|
|
13165
|
+
const parsed = parseJudgeResult(extractResponseText(payload));
|
|
13166
|
+
if (!parsed) {
|
|
13167
|
+
return {
|
|
13168
|
+
...input3,
|
|
13169
|
+
judgeError: "invalid_response"
|
|
13170
|
+
};
|
|
13171
|
+
}
|
|
13172
|
+
return {
|
|
13173
|
+
...input3,
|
|
13174
|
+
judgeError: void 0,
|
|
13175
|
+
judgeScore: parsed.judgeScore,
|
|
13176
|
+
calibrationNotes: parsed.calibrationNotes ?? input3.calibrationNotes,
|
|
13177
|
+
judgeFindings: parsed.judgeFindings ?? input3.judgeFindings
|
|
13178
|
+
};
|
|
13179
|
+
} catch (error) {
|
|
13180
|
+
return {
|
|
13181
|
+
...input3,
|
|
13182
|
+
judgeError: error?.name === "TimeoutError" ? "timeout" : error?.message || "request_failed"
|
|
13183
|
+
};
|
|
13184
|
+
}
|
|
13185
|
+
}
|
|
13186
|
+
async function runOfflineTaskJudge(options) {
|
|
13187
|
+
const tasks = options.tasks ?? DEFAULT_OFFLINE_EVALUATION_TASKS;
|
|
13188
|
+
const taskMap = new Map(tasks.map((task) => [task.id, task]));
|
|
13189
|
+
const judgeModel = options.judgeModel.trim();
|
|
13190
|
+
if (!judgeModel) {
|
|
13191
|
+
throw new Error("LLM \u88C1\u5224\u9700\u8981 judgeModel\u3002");
|
|
13192
|
+
}
|
|
13193
|
+
if (!options.baseUrl?.trim()) {
|
|
13194
|
+
throw new Error("LLM \u88C1\u5224\u9700\u8981 baseUrl\u3002");
|
|
13195
|
+
}
|
|
13196
|
+
const inputs = new Array(options.inputs.length);
|
|
13197
|
+
let nextIndex = 0;
|
|
13198
|
+
const concurrency = Math.max(1, Math.min(Math.floor(options.concurrency ?? 2), 8));
|
|
13199
|
+
const sharedOptions = {
|
|
13200
|
+
judgeModel,
|
|
13201
|
+
baseUrl: options.baseUrl.trim(),
|
|
13202
|
+
apiKey: options.apiKey?.trim() || void 0,
|
|
13203
|
+
timeoutMs: Math.max(0, Math.floor(options.timeoutMs ?? 3e4)),
|
|
13204
|
+
maxTokens: Math.max(1, Math.floor(options.maxTokens ?? 256)),
|
|
13205
|
+
fetchFn: options.fetchFn ?? fetch
|
|
13206
|
+
};
|
|
13207
|
+
async function worker() {
|
|
13208
|
+
while (nextIndex < options.inputs.length) {
|
|
13209
|
+
const currentIndex = nextIndex;
|
|
13210
|
+
nextIndex += 1;
|
|
13211
|
+
const input3 = options.inputs[currentIndex];
|
|
13212
|
+
inputs[currentIndex] = await runJudgeJob(taskMap.get(input3.taskId), input3, sharedOptions);
|
|
13213
|
+
}
|
|
13214
|
+
}
|
|
13215
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, options.inputs.length) }, () => worker()));
|
|
13216
|
+
return {
|
|
13217
|
+
inputs,
|
|
13218
|
+
report: runOfflineTaskEvaluation(inputs, tasks)
|
|
13219
|
+
};
|
|
13220
|
+
}
|
|
12408
13221
|
async function runOfflineTaskBenchmark(options) {
|
|
12409
13222
|
const tasks = options.tasks ?? DEFAULT_OFFLINE_EVALUATION_TASKS;
|
|
12410
13223
|
const models = options.models.map((model) => model.trim()).filter(Boolean);
|
|
@@ -12434,6 +13247,19 @@ async function runOfflineTaskBenchmark(options) {
|
|
|
12434
13247
|
}
|
|
12435
13248
|
}
|
|
12436
13249
|
await Promise.all(Array.from({ length: Math.min(concurrency, jobs.length) }, () => worker()));
|
|
13250
|
+
if (options.judgeModel?.trim()) {
|
|
13251
|
+
return runOfflineTaskJudge({
|
|
13252
|
+
inputs,
|
|
13253
|
+
tasks,
|
|
13254
|
+
judgeModel: options.judgeModel,
|
|
13255
|
+
baseUrl: sharedOptions.baseUrl,
|
|
13256
|
+
apiKey: sharedOptions.apiKey,
|
|
13257
|
+
timeoutMs: sharedOptions.timeoutMs,
|
|
13258
|
+
concurrency,
|
|
13259
|
+
maxTokens: options.judgeMaxTokens ?? 256,
|
|
13260
|
+
fetchFn: sharedOptions.fetchFn
|
|
13261
|
+
});
|
|
13262
|
+
}
|
|
12437
13263
|
return {
|
|
12438
13264
|
inputs,
|
|
12439
13265
|
report: runOfflineTaskEvaluation(inputs, tasks)
|
|
@@ -12471,7 +13297,11 @@ function buildOfflineTaskManifest(tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
|
|
|
12471
13297
|
taskId: task.id,
|
|
12472
13298
|
model: "<provider,model>",
|
|
12473
13299
|
output: "<model output>",
|
|
12474
|
-
latencyMs: 0
|
|
13300
|
+
latencyMs: 0,
|
|
13301
|
+
humanScore: null,
|
|
13302
|
+
judgeScore: null,
|
|
13303
|
+
calibrationNotes: null,
|
|
13304
|
+
judgeFindings: []
|
|
12475
13305
|
}
|
|
12476
13306
|
}))
|
|
12477
13307
|
};
|
|
@@ -12510,6 +13340,13 @@ function formatOfflineTaskEvaluationReport(report) {
|
|
|
12510
13340
|
if (dimensions.length) {
|
|
12511
13341
|
lines.push(`Average dimensions: ${formatDimensionSummary(report.averageDimensionScores)}`);
|
|
12512
13342
|
}
|
|
13343
|
+
if (report.calibrationSummary.calibratedRuns) {
|
|
13344
|
+
lines.push(
|
|
13345
|
+
`Calibration: ${report.calibrationSummary.calibratedRuns} runs, human ${report.calibrationSummary.averageHumanScore.toFixed(2)}, judge ${report.calibrationSummary.averageJudgeScore.toFixed(2)}, delta ${report.calibrationSummary.averageRubricDelta.toFixed(2)}`
|
|
13346
|
+
);
|
|
13347
|
+
} else {
|
|
13348
|
+
lines.push("Calibration: none (add humanScore or judgeScore to compare rubric with human/LLM judge)");
|
|
13349
|
+
}
|
|
12513
13350
|
if (report.missingTaskIds.length) {
|
|
12514
13351
|
lines.push(`Missing task ids: ${report.missingTaskIds.join(", ")}`);
|
|
12515
13352
|
}
|
|
@@ -12528,6 +13365,12 @@ function formatOfflineTaskEvaluationReport(report) {
|
|
|
12528
13365
|
lines.push(`- ${run2.taskId} -> ${run2.model}: ${run2.findings.length ? run2.findings.join(", ") : "quality_below_threshold"}`);
|
|
12529
13366
|
}
|
|
12530
13367
|
}
|
|
13368
|
+
if (report.calibrationSummary.highDisagreementRuns.length) {
|
|
13369
|
+
lines.push("Calibration disagreements:");
|
|
13370
|
+
for (const run2 of report.calibrationSummary.highDisagreementRuns) {
|
|
13371
|
+
lines.push(`- ${run2.taskId} -> ${run2.model}: rubric ${run2.qualityScore.toFixed(2)}, calibration ${run2.calibrationScore.toFixed(2)}, delta ${run2.deltaFromQuality.toFixed(2)}`);
|
|
13372
|
+
}
|
|
13373
|
+
}
|
|
12531
13374
|
return lines.join("\n");
|
|
12532
13375
|
}
|
|
12533
13376
|
var DEFAULT_OFFLINE_EVALUATION_TASKS;
|
|
@@ -12638,6 +13481,16 @@ function getArgValue(flag, shortFlag) {
|
|
|
12638
13481
|
const value = index !== -1 ? args[index + 1] : void 0;
|
|
12639
13482
|
return value && !value.startsWith("-") ? value : void 0;
|
|
12640
13483
|
}
|
|
13484
|
+
function getOptionalArgValue(flag, label) {
|
|
13485
|
+
if (!hasArg2(flag)) {
|
|
13486
|
+
return void 0;
|
|
13487
|
+
}
|
|
13488
|
+
const value = getArgValue(flag);
|
|
13489
|
+
if (!value) {
|
|
13490
|
+
throw new Error(`${label} \u9700\u8981\u63D0\u4F9B\u503C\uFF1A${flag} <value>`);
|
|
13491
|
+
}
|
|
13492
|
+
return value;
|
|
13493
|
+
}
|
|
12641
13494
|
function parsePortValue(portValue, sourceLabel) {
|
|
12642
13495
|
const trimmed = portValue.trim();
|
|
12643
13496
|
if (!/^\d+$/.test(trimmed)) {
|
|
@@ -12685,7 +13538,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
12685
13538
|
\u547D\u4EE4\uFF1A
|
|
12686
13539
|
setup \u68C0\u6D4B\u5E76\u590D\u7528\u5DF2\u6709\u914D\u7F6E\uFF0C\u5FC5\u8981\u65F6\u8FC1\u79FB\u65E7\u914D\u7F6E\u6216\u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
|
|
12687
13540
|
doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
|
|
12688
|
-
eval \u79BB\u7EBF\u8BC4\u6D4B\u56FA\u5B9A\u4EFB\u52A1\u96C6\u8F93\u51FA\uFF08--input
|
|
13541
|
+
eval \u79BB\u7EBF\u8BC4\u6D4B\u56FA\u5B9A\u4EFB\u52A1\u96C6\u8F93\u51FA\uFF08--input / --tasks / --run / --judge-model\uFF09
|
|
12689
13542
|
init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
|
|
12690
13543
|
deploy \u751F\u6210\u90E8\u7F72\u5165\u53E3\u914D\u7F6E\uFF08\u5F53\u524D\u652F\u6301 deploy init --target server\uFF09
|
|
12691
13544
|
start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
|
|
@@ -12709,6 +13562,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
12709
13562
|
ctr eval --tasks # \u67E5\u770B\u56FA\u5B9A\u8BC4\u6D4B\u4EFB\u52A1\u3001prompt \u548C rubric
|
|
12710
13563
|
ctr eval --input results.json # \u7528\u56FA\u5B9A\u4EFB\u52A1\u96C6 rubric \u8BC4\u6D4B\u591A\u6A21\u578B\u8F93\u51FA\u7ED3\u679C
|
|
12711
13564
|
ctr eval --run --models "sonnet;haiku" # \u81EA\u52A8\u8C03\u7528 CTR /v1/messages \u540E\u8BC4\u6D4B
|
|
13565
|
+
ctr eval --run --models "sonnet;haiku" --judge-model sonnet # \u81EA\u52A8\u8FFD\u52A0 LLM \u88C1\u5224\u5206
|
|
12712
13566
|
ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
|
|
12713
13567
|
ctr deploy init --target server # \u751F\u6210\u5B89\u5168\u9ED8\u8BA4\u7684 server \u90E8\u7F72\u914D\u7F6E
|
|
12714
13568
|
ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
|
|
@@ -12842,13 +13696,16 @@ async function runOfflineEvaluationCli() {
|
|
|
12842
13696
|
const config = readConfigForCliStatus();
|
|
12843
13697
|
const baseUrl = getArgValue("--base-url") || `http://127.0.0.1:${getPort()}`;
|
|
12844
13698
|
const apiKey = getArgValue("--api-key") || getLocalClaudeProxyToken(config);
|
|
13699
|
+
const judgeModel = getOptionalArgValue("--judge-model", "judge-model");
|
|
12845
13700
|
const result = await runOfflineTaskBenchmark({
|
|
12846
13701
|
models,
|
|
12847
13702
|
baseUrl,
|
|
12848
13703
|
apiKey,
|
|
12849
13704
|
timeoutMs: parsePositiveIntegerArg("--timeout-ms", void 0, 3e4, "timeout-ms"),
|
|
12850
13705
|
concurrency: parsePositiveIntegerArg("--concurrency", void 0, 2, "concurrency"),
|
|
12851
|
-
maxTokens: parsePositiveIntegerArg("--max-tokens", void 0, 768, "max-tokens")
|
|
13706
|
+
maxTokens: parsePositiveIntegerArg("--max-tokens", void 0, 768, "max-tokens"),
|
|
13707
|
+
judgeModel,
|
|
13708
|
+
judgeMaxTokens: parsePositiveIntegerArg("--judge-max-tokens", void 0, 256, "judge-max-tokens")
|
|
12852
13709
|
});
|
|
12853
13710
|
if (hasArg2("--json")) {
|
|
12854
13711
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -12871,6 +13728,25 @@ async function runOfflineEvaluationCli() {
|
|
|
12871
13728
|
}
|
|
12872
13729
|
try {
|
|
12873
13730
|
const inputs = readOfflineEvaluationInputs(inputPath);
|
|
13731
|
+
const judgeModel = getOptionalArgValue("--judge-model", "judge-model");
|
|
13732
|
+
if (judgeModel) {
|
|
13733
|
+
const config = readConfigForCliStatus();
|
|
13734
|
+
const result = await runOfflineTaskJudge({
|
|
13735
|
+
inputs,
|
|
13736
|
+
judgeModel,
|
|
13737
|
+
baseUrl: getArgValue("--base-url") || `http://127.0.0.1:${getPort()}`,
|
|
13738
|
+
apiKey: getArgValue("--api-key") || getLocalClaudeProxyToken(config),
|
|
13739
|
+
timeoutMs: parsePositiveIntegerArg("--timeout-ms", void 0, 3e4, "timeout-ms"),
|
|
13740
|
+
concurrency: parsePositiveIntegerArg("--concurrency", void 0, 2, "concurrency"),
|
|
13741
|
+
maxTokens: parsePositiveIntegerArg("--judge-max-tokens", void 0, 256, "judge-max-tokens")
|
|
13742
|
+
});
|
|
13743
|
+
if (hasArg2("--json")) {
|
|
13744
|
+
console.log(JSON.stringify(result, null, 2));
|
|
13745
|
+
return;
|
|
13746
|
+
}
|
|
13747
|
+
console.log(formatOfflineTaskEvaluationReport(result.report));
|
|
13748
|
+
return;
|
|
13749
|
+
}
|
|
12874
13750
|
const report = runOfflineTaskEvaluation(inputs);
|
|
12875
13751
|
if (hasArg2("--json")) {
|
|
12876
13752
|
console.log(JSON.stringify(report, null, 2));
|