@peterwangze/claude-trigger-router 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -125,6 +125,14 @@ var init_constants = __esm({
125
125
  });
126
126
 
127
127
  // src/auth/api-keys.ts
128
+ function normalizeKnownScopes(input3) {
129
+ if (!Array.isArray(input3)) {
130
+ return [];
131
+ }
132
+ return Array.from(new Set(
133
+ input3.map((item) => String(item).trim()).filter((item) => VALID_SCOPES.includes(item))
134
+ ));
135
+ }
128
136
  function createSecret() {
129
137
  const token = (0, import_crypto.randomBytes)(24).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
130
138
  return `ctr_${token}`;
@@ -147,9 +155,7 @@ function normalizeManagedApiKeyScopes(input3) {
147
155
  if (!Array.isArray(input3) || input3.length === 0) {
148
156
  return ["client"];
149
157
  }
150
- const scopes = Array.from(new Set(
151
- input3.map((item) => String(item).trim()).filter((item) => VALID_SCOPES.includes(item))
152
- ));
158
+ const scopes = normalizeKnownScopes(input3);
153
159
  return scopes.length ? scopes : ["client"];
154
160
  }
155
161
  function validateManagedApiKeyScopes(input3) {
@@ -205,7 +211,7 @@ function sanitizeManagedApiKey(record, now = /* @__PURE__ */ new Date()) {
205
211
  label: record.label,
206
212
  keyPrefix: record.key_prefix,
207
213
  keySuffix: record.key_suffix,
208
- scopes: record.scopes,
214
+ scopes: normalizeKnownScopes(record.scopes),
209
215
  createdAt: record.created_at,
210
216
  expiresAt: record.expires_at,
211
217
  revokedAt: record.revoked_at,
@@ -226,14 +232,18 @@ function managedApiKeySummary(config, now = /* @__PURE__ */ new Date()) {
226
232
  };
227
233
  }
228
234
  function scopeAllows(scopes, required) {
229
- if (scopes.includes("admin")) {
235
+ const normalizedScopes = normalizeKnownScopes(scopes);
236
+ if (normalizedScopes.includes("admin")) {
230
237
  return true;
231
238
  }
232
239
  if (required === "read-only") {
233
- return scopes.includes("read-only");
240
+ return normalizedScopes.includes("read-only") || normalizedScopes.includes("operator");
241
+ }
242
+ if (required === "operator") {
243
+ return normalizedScopes.includes("operator");
234
244
  }
235
245
  if (required === "client") {
236
- return scopes.includes("client");
246
+ return normalizedScopes.includes("client");
237
247
  }
238
248
  return false;
239
249
  }
@@ -259,14 +269,15 @@ function verifyApiKey(config, providedKey, required = "client", now = /* @__PURE
259
269
  if (!isManagedApiKeyActive(record, now)) {
260
270
  return { ok: false, source: "managed", keyId: record.id, reason: "expired" };
261
271
  }
262
- if (!scopeAllows(record.scopes, required)) {
272
+ const scopes = normalizeKnownScopes(record.scopes);
273
+ if (!scopeAllows(scopes, required)) {
263
274
  return { ok: false, source: "managed", keyId: record.id, reason: "insufficient_scope" };
264
275
  }
265
276
  return {
266
277
  ok: true,
267
278
  source: "managed",
268
279
  keyId: record.id,
269
- scopes: record.scopes,
280
+ scopes,
270
281
  quota: record.quota
271
282
  };
272
283
  }
@@ -286,7 +297,7 @@ var init_api_keys = __esm({
286
297
  "src/auth/api-keys.ts"() {
287
298
  "use strict";
288
299
  import_crypto = require("crypto");
289
- VALID_SCOPES = ["admin", "client", "read-only"];
300
+ VALID_SCOPES = ["admin", "operator", "client", "read-only"];
290
301
  AuthAuditStore = class {
291
302
  constructor(max = 200) {
292
303
  this.max = max;
@@ -511,6 +522,9 @@ var init_api_keys = __esm({
511
522
  function trimTrailingSlash(value) {
512
523
  return value.replace(/\/+$/, "");
513
524
  }
525
+ function restoreTrailingSlash(value, shouldRestore) {
526
+ return shouldRestore && value ? `${value}/` : value;
527
+ }
514
528
  function trimString(value) {
515
529
  return typeof value === "string" ? value.trim() : "";
516
530
  }
@@ -535,12 +549,13 @@ function inferInterfaceFromApiEndpoint(api, modelName) {
535
549
  return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
536
550
  }
537
551
  function normalizeEndpointPath(pathname, modelInterface) {
552
+ const hadExplicitTrailingSlash = pathname.length > 1 && /\/+$/.test(pathname);
538
553
  const trimmedPath = trimTrailingSlash(pathname || "");
539
554
  const normalizedPath = trimmedPath || "";
540
555
  const lowerPath = normalizedPath.toLowerCase();
541
556
  if (modelInterface === "anthropic") {
542
557
  if (lowerPath.endsWith("/v1/messages") || lowerPath.endsWith("/messages")) {
543
- return normalizedPath || "/v1/messages";
558
+ return restoreTrailingSlash(normalizedPath || "/v1/messages", hadExplicitTrailingSlash);
544
559
  }
545
560
  if (lowerPath.endsWith("/v1")) {
546
561
  return `${normalizedPath}/messages`;
@@ -548,10 +563,10 @@ function normalizeEndpointPath(pathname, modelInterface) {
548
563
  if (!normalizedPath) {
549
564
  return "/v1/messages";
550
565
  }
551
- return normalizedPath;
566
+ return restoreTrailingSlash(normalizedPath, hadExplicitTrailingSlash);
552
567
  }
553
568
  if (lowerPath.endsWith("/chat/completions")) {
554
- return normalizedPath || "/chat/completions";
569
+ return restoreTrailingSlash(normalizedPath || "/chat/completions", hadExplicitTrailingSlash);
555
570
  }
556
571
  if (lowerPath.endsWith("/v1")) {
557
572
  return `${normalizedPath}/chat/completions`;
@@ -559,7 +574,7 @@ function normalizeEndpointPath(pathname, modelInterface) {
559
574
  if (!normalizedPath) {
560
575
  return "/v1/chat/completions";
561
576
  }
562
- return normalizedPath;
577
+ return restoreTrailingSlash(normalizedPath, hadExplicitTrailingSlash);
563
578
  }
564
579
  function normalizeApiEndpoint(api, explicitInterface) {
565
580
  const trimmed = api?.trim() || "";
@@ -573,8 +588,7 @@ function normalizeApiEndpoint(api, explicitInterface) {
573
588
  return url.toString();
574
589
  } catch {
575
590
  const [base, suffix = ""] = trimmed.split(/([?#].*)/, 2);
576
- const normalizedBase = trimTrailingSlash(base);
577
- const normalizedPath = normalizeEndpointPath(normalizedBase, modelInterface);
591
+ const normalizedPath = normalizeEndpointPath(base, modelInterface);
578
592
  return `${normalizedPath}${suffix}`;
579
593
  }
580
594
  }
@@ -667,6 +681,193 @@ var init_schema = __esm({
667
681
  }
668
682
  });
669
683
 
684
+ // src/models/pool-health.ts
685
+ var ModelPoolHealthStore, modelPoolHealthStore;
686
+ var init_pool_health = __esm({
687
+ "src/models/pool-health.ts"() {
688
+ "use strict";
689
+ ModelPoolHealthStore = class {
690
+ constructor(cooldownMs = 6e4, circuitBreakerFailureThreshold = 3, circuitBreakerCooldownMs = 3e5, latencyWindowSize = 20) {
691
+ this.cooldownMs = cooldownMs;
692
+ this.circuitBreakerFailureThreshold = circuitBreakerFailureThreshold;
693
+ this.circuitBreakerCooldownMs = circuitBreakerCooldownMs;
694
+ this.latencyWindowSize = latencyWindowSize;
695
+ }
696
+ states = /* @__PURE__ */ new Map();
697
+ changeListener;
698
+ clear() {
699
+ this.states.clear();
700
+ this.notifyChange();
701
+ }
702
+ setChangeListener(listener) {
703
+ this.changeListener = listener;
704
+ }
705
+ hydrate(payload) {
706
+ this.states.clear();
707
+ if (!payload || !Array.isArray(payload.endpoints)) {
708
+ return;
709
+ }
710
+ for (const entry of payload.endpoints) {
711
+ if (!entry || typeof entry.modelId !== "string" || typeof entry.endpointId !== "string") {
712
+ continue;
713
+ }
714
+ const modelId = entry.modelId.trim();
715
+ const endpointId = entry.endpointId.trim();
716
+ if (!modelId || !endpointId) {
717
+ continue;
718
+ }
719
+ this.states.set(this.key(modelId, endpointId), {
720
+ failureCount: this.readFiniteNumber(entry.failureCount) ?? 0,
721
+ successCount: this.readFiniteNumber(entry.successCount) ?? 0,
722
+ lastFailureAt: this.readFiniteNumber(entry.lastFailureAt),
723
+ lastSuccessAt: this.readFiniteNumber(entry.lastSuccessAt),
724
+ cooldownUntil: this.readFiniteNumber(entry.cooldownUntil),
725
+ circuitOpenUntil: this.readFiniteNumber(entry.circuitOpenUntil),
726
+ latencySamples: this.normalizeLatencySamples(entry.latencySamples)
727
+ });
728
+ }
729
+ }
730
+ exportForPersistence(now = /* @__PURE__ */ new Date()) {
731
+ const endpoints = Array.from(this.states.entries()).map(([key, state]) => {
732
+ const [modelId, endpointId] = key.split("\0");
733
+ return {
734
+ modelId,
735
+ endpointId,
736
+ failureCount: state.failureCount,
737
+ successCount: state.successCount,
738
+ lastFailureAt: state.lastFailureAt,
739
+ lastSuccessAt: state.lastSuccessAt,
740
+ cooldownUntil: state.cooldownUntil,
741
+ circuitOpenUntil: state.circuitOpenUntil,
742
+ latencySamples: state.latencySamples
743
+ };
744
+ });
745
+ return {
746
+ version: 1,
747
+ updatedAt: now.toISOString(),
748
+ endpoints
749
+ };
750
+ }
751
+ recordFailure(modelId, endpointId, now = Date.now()) {
752
+ const key = this.key(modelId, endpointId);
753
+ const current = this.states.get(key) ?? {
754
+ failureCount: 0,
755
+ successCount: 0
756
+ };
757
+ const failureCount = current.failureCount + 1;
758
+ const shouldOpenCircuit = failureCount >= this.circuitBreakerFailureThreshold;
759
+ const next = {
760
+ ...current,
761
+ failureCount,
762
+ lastFailureAt: now,
763
+ cooldownUntil: now + this.cooldownMs,
764
+ circuitOpenUntil: shouldOpenCircuit ? now + this.circuitBreakerCooldownMs : current.circuitOpenUntil
765
+ };
766
+ this.states.set(key, next);
767
+ this.notifyChange();
768
+ return this.toSnapshot(modelId, endpointId, next, now);
769
+ }
770
+ recordSuccess(modelId, endpointId, now = Date.now(), latencyMs) {
771
+ const key = this.key(modelId, endpointId);
772
+ const current = this.states.get(key) ?? {
773
+ failureCount: 0,
774
+ successCount: 0
775
+ };
776
+ const latencySamples = this.appendLatencySample(current.latencySamples, latencyMs, now);
777
+ const next = {
778
+ ...current,
779
+ failureCount: 0,
780
+ successCount: current.successCount + 1,
781
+ lastSuccessAt: now,
782
+ cooldownUntil: void 0,
783
+ circuitOpenUntil: void 0,
784
+ ...latencySamples ? { latencySamples } : {}
785
+ };
786
+ this.states.set(key, next);
787
+ this.notifyChange();
788
+ return this.toSnapshot(modelId, endpointId, next, now);
789
+ }
790
+ getSnapshot(modelId, endpointId, now = Date.now()) {
791
+ return this.toSnapshot(
792
+ modelId,
793
+ endpointId,
794
+ this.states.get(this.key(modelId, endpointId)),
795
+ now
796
+ );
797
+ }
798
+ isEndpointAvailable(modelId, endpointId, now = Date.now()) {
799
+ return this.getSnapshot(modelId, endpointId, now).status === "healthy";
800
+ }
801
+ key(modelId, endpointId) {
802
+ return `${modelId}\0${endpointId}`;
803
+ }
804
+ appendLatencySample(samples, latencyMs, recordedAt) {
805
+ if (typeof latencyMs !== "number" || !Number.isFinite(latencyMs) || latencyMs < 0) {
806
+ return samples;
807
+ }
808
+ return [
809
+ ...samples ?? [],
810
+ {
811
+ latencyMs,
812
+ recordedAt
813
+ }
814
+ ].slice(-this.latencyWindowSize);
815
+ }
816
+ normalizeLatencySamples(samples) {
817
+ if (!Array.isArray(samples)) {
818
+ return void 0;
819
+ }
820
+ const normalized = samples.map((sample) => ({
821
+ latencyMs: this.readFiniteNumber(sample?.latencyMs),
822
+ recordedAt: this.readFiniteNumber(sample?.recordedAt)
823
+ })).filter(
824
+ (sample) => sample.latencyMs !== void 0 && sample.latencyMs >= 0 && sample.recordedAt !== void 0
825
+ ).slice(-this.latencyWindowSize);
826
+ return normalized.length ? normalized : void 0;
827
+ }
828
+ readFiniteNumber(value) {
829
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
830
+ }
831
+ notifyChange() {
832
+ this.changeListener?.(this.exportForPersistence());
833
+ }
834
+ toLatencyWindow(samples) {
835
+ if (!samples?.length) {
836
+ return void 0;
837
+ }
838
+ const latencyValues = samples.map((sample) => sample.latencyMs);
839
+ const recordedAtValues = samples.map((sample) => sample.recordedAt);
840
+ return {
841
+ sampleCount: samples.length,
842
+ averageMs: latencyValues.reduce((sum, value) => sum + value, 0) / latencyValues.length,
843
+ lastMs: samples[samples.length - 1].latencyMs,
844
+ windowStartedAt: Math.min(...recordedAtValues),
845
+ windowEndedAt: Math.max(...recordedAtValues)
846
+ };
847
+ }
848
+ toSnapshot(modelId, endpointId, state, now = Date.now()) {
849
+ const cooldownUntil = state?.cooldownUntil;
850
+ const circuitOpenUntil = state?.circuitOpenUntil;
851
+ const inCooldown = typeof cooldownUntil === "number" && cooldownUntil > now;
852
+ const circuitOpen = typeof circuitOpenUntil === "number" && circuitOpenUntil > now;
853
+ return {
854
+ modelId,
855
+ endpointId,
856
+ status: circuitOpen ? "open" : inCooldown ? "cooldown" : "healthy",
857
+ failureCount: state?.failureCount ?? 0,
858
+ successCount: state?.successCount ?? 0,
859
+ lastFailureAt: state?.lastFailureAt,
860
+ lastSuccessAt: state?.lastSuccessAt,
861
+ ...inCooldown ? { cooldownUntil } : {},
862
+ ...circuitOpen ? { circuitOpenUntil } : {},
863
+ ...state?.latencySamples?.length ? { latency: this.toLatencyWindow(state.latencySamples) } : {}
864
+ };
865
+ }
866
+ };
867
+ modelPoolHealthStore = new ModelPoolHealthStore();
868
+ }
869
+ });
870
+
670
871
  // src/models/compile.ts
671
872
  function inferTransformer(protocol) {
672
873
  if (protocol === "openai") {
@@ -735,6 +936,8 @@ function describeDispatchFormat(format) {
735
936
  }
736
937
  function buildCompiledCapabilities(item, modelInterface) {
737
938
  const reasoningSupported = item.metadata?.supports_reasoning !== false;
939
+ const contextWindowTokens = readMetadataNumber(item.metadata, "context_window_tokens");
940
+ const safeInputTokens = readMetadataNumber(item.metadata, "safe_input_tokens");
738
941
  return {
739
942
  thinking: {
740
943
  supported: reasoningSupported,
@@ -742,7 +945,9 @@ function buildCompiledCapabilities(item, modelInterface) {
742
945
  },
743
946
  tools: item.metadata?.supports_tools !== false,
744
947
  images: item.metadata?.supports_images !== false,
745
- systemMessageStyle: modelInterface
948
+ systemMessageStyle: modelInterface,
949
+ ...contextWindowTokens ? { contextWindowTokens } : {},
950
+ ...safeInputTokens ? { safeInputTokens } : {}
746
951
  };
747
952
  }
748
953
  function readMetadataString(metadata, key) {
@@ -787,7 +992,7 @@ function sanitizeProviderName(value) {
787
992
  const sanitized = value.trim().replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
788
993
  return sanitized || "endpoint";
789
994
  }
790
- function buildCompiledModelRefFromPoolEndpoint(endpoint, thinking, compatibilityProfile) {
995
+ function buildCompiledModelRefFromPoolEndpoint(endpoint, thinking, compatibilityProfile, strategy) {
791
996
  return {
792
997
  id: endpoint.modelId,
793
998
  providerName: endpoint.providerName,
@@ -802,10 +1007,36 @@ function buildCompiledModelRefFromPoolEndpoint(endpoint, thinking, compatibility
802
1007
  modelPool: {
803
1008
  modelId: endpoint.modelId,
804
1009
  endpointId: endpoint.id,
805
- strategy: "priority"
1010
+ strategy
806
1011
  }
807
1012
  };
808
1013
  }
1014
+ function getRegistrationPoolStrategy(config) {
1015
+ return config.Registration?.strategy === "least-latency" ? "least-latency" : "priority";
1016
+ }
1017
+ function latencyScore(endpoint) {
1018
+ const averageMs = endpoint.health.latency?.averageMs;
1019
+ return typeof averageMs === "number" && Number.isFinite(averageMs) ? averageMs : void 0;
1020
+ }
1021
+ function sortEndpointsForStrategy(endpoints, strategy) {
1022
+ if (strategy !== "least-latency") {
1023
+ return [...endpoints].sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
1024
+ }
1025
+ return [...endpoints].sort((a, b) => {
1026
+ const leftLatency = latencyScore(a);
1027
+ const rightLatency = latencyScore(b);
1028
+ if (leftLatency !== void 0 && rightLatency !== void 0 && leftLatency !== rightLatency) {
1029
+ return leftLatency - rightLatency;
1030
+ }
1031
+ if (leftLatency !== void 0 && rightLatency === void 0) {
1032
+ return -1;
1033
+ }
1034
+ if (leftLatency === void 0 && rightLatency !== void 0) {
1035
+ return 1;
1036
+ }
1037
+ return a.priority - b.priority || a.id.localeCompare(b.id);
1038
+ });
1039
+ }
809
1040
  function buildRegistrationModelPools(config) {
810
1041
  const registration = config.Registration;
811
1042
  if (!registration?.enabled || !Array.isArray(registration.models) || registration.models.length === 0) {
@@ -821,6 +1052,7 @@ function buildRegistrationModelPools(config) {
821
1052
  const providers = [];
822
1053
  const modelMap = {};
823
1054
  const pools = {};
1055
+ const strategy = getRegistrationPoolStrategy(config);
824
1056
  registration.models.forEach((rawItem, index) => {
825
1057
  const item = normalizeModelEndpointConfig(rawItem);
826
1058
  if (!item.id) {
@@ -860,6 +1092,7 @@ function buildRegistrationModelPools(config) {
860
1092
  upstreamAuthConfigured: Boolean(upstreamService?.auth_token),
861
1093
  priority: poolPriority,
862
1094
  enabled,
1095
+ health: modelPoolHealthStore.getSnapshot(item.id, endpointId),
863
1096
  capabilities,
864
1097
  source: "registration"
865
1098
  };
@@ -873,11 +1106,12 @@ function buildRegistrationModelPools(config) {
873
1106
  modelMap[endpoint.legacyRef] = buildCompiledModelRefFromPoolEndpoint(
874
1107
  endpoint,
875
1108
  item.thinking,
876
- compatibilityProfile
1109
+ compatibilityProfile,
1110
+ strategy
877
1111
  );
878
1112
  const pool = pools[item.id] ?? {
879
1113
  modelId: item.id,
880
- strategy: "priority",
1114
+ strategy,
881
1115
  endpoints: [],
882
1116
  warnings: []
883
1117
  };
@@ -887,7 +1121,10 @@ function buildRegistrationModelPools(config) {
887
1121
  });
888
1122
  Object.values(pools).forEach((pool) => {
889
1123
  pool.endpoints.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
890
- pool.activeEndpointId = pool.endpoints.find((endpoint) => endpoint.enabled)?.id;
1124
+ const selectionOrder = sortEndpointsForStrategy(pool.endpoints, pool.strategy);
1125
+ pool.activeEndpointId = selectionOrder.find(
1126
+ (endpoint) => endpoint.enabled && modelPoolHealthStore.isEndpointAvailable(pool.modelId, endpoint.id)
1127
+ )?.id ?? pool.endpoints.find((endpoint) => endpoint.enabled)?.id;
891
1128
  const activeEndpoint = pool.endpoints.find((endpoint) => endpoint.id === pool.activeEndpointId);
892
1129
  if (activeEndpoint) {
893
1130
  modelMap[pool.modelId] = modelMap[activeEndpoint.legacyRef];
@@ -1020,6 +1257,33 @@ function getCompiledModelRef(config, ref) {
1020
1257
  (item) => item.providerName === providerName && item.modelName === modelName
1021
1258
  );
1022
1259
  }
1260
+ function getModelPoolFallbackCandidate(config, selection) {
1261
+ if (!selection?.modelId || !selection.endpointId) {
1262
+ return void 0;
1263
+ }
1264
+ const registry = buildModelRegistry(config);
1265
+ const pool = registry.modelPools[selection.modelId];
1266
+ if (!pool) {
1267
+ return void 0;
1268
+ }
1269
+ const enabledEndpoints = pool.endpoints.filter((endpoint) => endpoint.enabled);
1270
+ const currentIndex = enabledEndpoints.findIndex((endpoint) => endpoint.id === selection.endpointId);
1271
+ const fallbackCandidates = pool.strategy === "least-latency" ? enabledEndpoints.filter((endpoint) => endpoint.id !== selection.endpointId) : currentIndex >= 0 ? enabledEndpoints.slice(currentIndex + 1) : enabledEndpoints.filter((endpoint) => endpoint.id !== selection.endpointId);
1272
+ const fallbackEndpoint = sortEndpointsForStrategy(fallbackCandidates, pool.strategy).find(
1273
+ (endpoint) => modelPoolHealthStore.isEndpointAvailable(pool.modelId, endpoint.id)
1274
+ );
1275
+ if (!fallbackEndpoint) {
1276
+ return void 0;
1277
+ }
1278
+ return {
1279
+ modelId: fallbackEndpoint.modelId,
1280
+ endpointId: fallbackEndpoint.id,
1281
+ strategy: pool.strategy,
1282
+ legacyRef: fallbackEndpoint.legacyRef,
1283
+ providerName: fallbackEndpoint.providerName,
1284
+ modelName: fallbackEndpoint.modelName
1285
+ };
1286
+ }
1023
1287
  function isKnownModelReference(config, ref) {
1024
1288
  if (!ref) {
1025
1289
  return false;
@@ -1093,6 +1357,7 @@ var init_compile = __esm({
1093
1357
  "src/models/compile.ts"() {
1094
1358
  "use strict";
1095
1359
  init_schema();
1360
+ init_pool_health();
1096
1361
  }
1097
1362
  });
1098
1363
 
@@ -1331,6 +1596,20 @@ function validateModelEndpointList(models, prefix, errors, options = {}) {
1331
1596
  if (thinking?.budget_tokens !== void 0 && thinking.budget_tokens <= 0) {
1332
1597
  errors.push(`${prefix}[${index}].thinking.budget_tokens must be greater than 0`);
1333
1598
  }
1599
+ const metadata = item.metadata;
1600
+ if (metadata?.context_window_tokens !== void 0) {
1601
+ if (!Number.isInteger(metadata.context_window_tokens) || metadata.context_window_tokens <= 0) {
1602
+ errors.push(`${prefix}[${index}].metadata.context_window_tokens must be a positive integer`);
1603
+ }
1604
+ }
1605
+ if (metadata?.safe_input_tokens !== void 0) {
1606
+ if (!Number.isInteger(metadata.safe_input_tokens) || metadata.safe_input_tokens <= 0) {
1607
+ errors.push(`${prefix}[${index}].metadata.safe_input_tokens must be a positive integer`);
1608
+ }
1609
+ }
1610
+ if (Number.isInteger(metadata?.context_window_tokens) && Number.isInteger(metadata?.safe_input_tokens) && metadata.safe_input_tokens > metadata.context_window_tokens) {
1611
+ errors.push(`${prefix}[${index}].metadata.safe_input_tokens must be less than or equal to context_window_tokens`);
1612
+ }
1334
1613
  });
1335
1614
  }
1336
1615
  function validateRegistrationUpstreamServices(services, errors) {
@@ -1477,6 +1756,9 @@ function validateConfig(config) {
1477
1756
  errors.push("Runtime.remote_service.base_url is required when remote_service is enabled");
1478
1757
  }
1479
1758
  const registration = config.Registration;
1759
+ if (registration?.strategy !== void 0 && !["priority", "least-latency"].includes(registration.strategy)) {
1760
+ errors.push('Registration.strategy must be one of "priority", "least-latency"');
1761
+ }
1480
1762
  if (registration?.nodes !== void 0) {
1481
1763
  errors.push("Registration.nodes is not supported yet; use Registration.models or Registration.upstream_services");
1482
1764
  }
@@ -1751,7 +2033,7 @@ function deriveRuntimeSmartRouterConfig(config, source) {
1751
2033
  enabled: hasExplicitStickyToggle ? baseSmartRouterConfig.sticky?.enabled ?? derivedSticky.enabled : true,
1752
2034
  alignment: {
1753
2035
  ...derivedSticky.alignment,
1754
- enabled: hasExplicitStickyToggle && (baseSmartRouterConfig.sticky?.alignment || config.Governance?.sticky?.alignment) ? baseSmartRouterConfig.sticky?.alignment?.enabled ?? derivedSticky.alignment?.enabled : true,
2036
+ enabled: hasExplicitStickyToggle && (baseSmartRouterConfig.sticky?.alignment || config.Governance?.sticky?.alignment) ? baseSmartRouterConfig.sticky?.alignment?.enabled ?? derivedSticky.alignment?.enabled : false,
1755
2037
  summarizer_model: baseSmartRouterConfig.sticky?.alignment?.summarizer_model || derivedSticky.alignment?.summarizer_model || defaultSummarizerModel
1756
2038
  }
1757
2039
  } : derivedSticky;
@@ -1939,6 +2221,7 @@ var init_config = __esm({
1939
2221
  };
1940
2222
  DEFAULT_REGISTRATION_CONFIG = {
1941
2223
  enabled: false,
2224
+ strategy: "priority",
1942
2225
  models: [],
1943
2226
  upstream_services: []
1944
2227
  };
@@ -2047,6 +2330,76 @@ async function probeRemoteServiceStatus(remoteService, timeoutMs = 800, fetchFn
2047
2330
  };
2048
2331
  }
2049
2332
  }
2333
+ async function probeRemoteRegistrationStatus(remoteService, timeoutMs = 800, fetchFn = fetch) {
2334
+ const enabled = Boolean(remoteService?.enabled);
2335
+ const baseUrl = remoteService?.base_url?.trim() ?? "";
2336
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
2337
+ if (!enabled) {
2338
+ return {
2339
+ enabled: false,
2340
+ configured: false,
2341
+ reachable: false,
2342
+ available: false,
2343
+ baseUrl: normalizedBaseUrl
2344
+ };
2345
+ }
2346
+ if (!baseUrl) {
2347
+ return {
2348
+ enabled: true,
2349
+ configured: false,
2350
+ reachable: false,
2351
+ available: false,
2352
+ baseUrl: normalizedBaseUrl,
2353
+ error: "Runtime.remote_service.base_url is required when remote_service is enabled"
2354
+ };
2355
+ }
2356
+ try {
2357
+ const headers = {};
2358
+ if (remoteService?.auth_token) {
2359
+ headers.Authorization = `Bearer ${remoteService.auth_token}`;
2360
+ }
2361
+ const res = await fetchFn(`${normalizedBaseUrl}${SERVICE_REGISTRATION_PATH}`, {
2362
+ headers,
2363
+ signal: AbortSignal.timeout(timeoutMs)
2364
+ });
2365
+ if (!res.ok) {
2366
+ return {
2367
+ enabled: true,
2368
+ configured: true,
2369
+ reachable: false,
2370
+ available: false,
2371
+ baseUrl: normalizedBaseUrl,
2372
+ error: `HTTP ${res.status}`
2373
+ };
2374
+ }
2375
+ const payload = await res.json();
2376
+ const info = payload && typeof payload === "object" ? payload : {};
2377
+ return {
2378
+ enabled: true,
2379
+ configured: true,
2380
+ reachable: true,
2381
+ available: true,
2382
+ baseUrl: normalizedBaseUrl,
2383
+ registrationEnabled: info.enabled === true,
2384
+ summary: {
2385
+ models: typeof info.summary?.models === "number" ? info.summary.models : 0,
2386
+ upstreamServices: typeof info.summary?.upstreamServices === "number" ? info.summary.upstreamServices : 0
2387
+ },
2388
+ models: Array.isArray(info.models) ? info.models : [],
2389
+ upstreamServices: Array.isArray(info.upstreamServices) ? info.upstreamServices : [],
2390
+ ...info.issueReport?.summary !== void 0 ? { issueSummary: info.issueReport.summary } : {}
2391
+ };
2392
+ } catch (error) {
2393
+ return {
2394
+ enabled: true,
2395
+ configured: true,
2396
+ reachable: false,
2397
+ available: false,
2398
+ baseUrl: normalizedBaseUrl,
2399
+ error: error?.message || String(error)
2400
+ };
2401
+ }
2402
+ }
2050
2403
  async function isTcpPortOccupied(port, timeoutMs = 500) {
2051
2404
  return new Promise((resolve) => {
2052
2405
  const socket = new import_net.Socket();
@@ -2082,7 +2435,7 @@ async function waitForService(port, timeoutMs = 5e3, options = {}) {
2082
2435
  }
2083
2436
  return false;
2084
2437
  }
2085
- var import_net, SERVICE_NAME, SERVICE_HEALTH_PATH, SERVICE_INFO_PATH;
2438
+ var import_net, SERVICE_NAME, SERVICE_HEALTH_PATH, SERVICE_INFO_PATH, SERVICE_REGISTRATION_PATH;
2086
2439
  var init_service_health = __esm({
2087
2440
  "src/service-health.ts"() {
2088
2441
  "use strict";
@@ -2090,6 +2443,7 @@ var init_service_health = __esm({
2090
2443
  SERVICE_NAME = "claude-trigger-router";
2091
2444
  SERVICE_HEALTH_PATH = "/api/health";
2092
2445
  SERVICE_INFO_PATH = "/api/service-info";
2446
+ SERVICE_REGISTRATION_PATH = "/api/registration";
2093
2447
  }
2094
2448
  });
2095
2449
 
@@ -2142,12 +2496,13 @@ function recordGovernanceTrace(trace) {
2142
2496
  governanceTraceStore.add(trace);
2143
2497
  return trace;
2144
2498
  }
2145
- var import_crypto2, import_fs2, import_lru_cache, import_path3, import_zlib, GovernanceTraceStore, governanceTraceStore;
2499
+ var import_crypto2, import_fs2, import_promises2, import_lru_cache, import_path3, import_zlib, GovernanceTraceStore, governanceTraceStore;
2146
2500
  var init_trace = __esm({
2147
2501
  "src/governance/trace.ts"() {
2148
2502
  "use strict";
2149
2503
  import_crypto2 = require("crypto");
2150
2504
  import_fs2 = require("fs");
2505
+ import_promises2 = require("fs/promises");
2151
2506
  import_lru_cache = require("lru-cache");
2152
2507
  import_path3 = require("path");
2153
2508
  import_zlib = require("zlib");
@@ -2160,6 +2515,9 @@ var init_trace = __esm({
2160
2515
  archiveDir;
2161
2516
  retainArchiveFiles;
2162
2517
  compressArchives;
2518
+ persistDebounceMs;
2519
+ persistTimer;
2520
+ persistQueue = Promise.resolve();
2163
2521
  constructor(options = {}) {
2164
2522
  const max = options.max ?? 500;
2165
2523
  const ttlMs = options.ttlMs ?? 1e3 * 60 * 60;
@@ -2173,11 +2531,12 @@ var init_trace = __esm({
2173
2531
  this.archiveDir = options.archiveDir ?? GOVERNANCE_TRACE_ARCHIVE_DIR;
2174
2532
  this.retainArchiveFiles = options.retainArchiveFiles ?? 5;
2175
2533
  this.compressArchives = options.compressArchives ?? true;
2534
+ this.persistDebounceMs = options.persistDebounceMs ?? 25;
2176
2535
  this.loadFromDisk();
2177
2536
  }
2178
2537
  add(trace) {
2179
2538
  this.cache.set(trace.requestId, { ...trace, routeReason: [...trace.routeReason] });
2180
- this.persistToDisk();
2539
+ this.schedulePersistToDisk();
2181
2540
  }
2182
2541
  get(requestId) {
2183
2542
  return this.cache.get(requestId);
@@ -2206,7 +2565,7 @@ var init_trace = __esm({
2206
2565
  }
2207
2566
  clear() {
2208
2567
  this.cache.clear();
2209
- this.persistToDisk();
2568
+ this.schedulePersistToDisk();
2210
2569
  this.clearArchives();
2211
2570
  }
2212
2571
  hydrate(traces) {
@@ -2214,7 +2573,15 @@ var init_trace = __esm({
2214
2573
  for (const trace of traces) {
2215
2574
  this.cache.set(trace.requestId, { ...trace, routeReason: [...trace.routeReason] });
2216
2575
  }
2217
- this.persistToDisk();
2576
+ this.schedulePersistToDisk();
2577
+ }
2578
+ async flushPersistence() {
2579
+ if (this.persistTimer) {
2580
+ clearTimeout(this.persistTimer);
2581
+ this.persistTimer = void 0;
2582
+ this.enqueuePersistToDisk();
2583
+ }
2584
+ await this.persistQueue;
2218
2585
  }
2219
2586
  listArchives(filters) {
2220
2587
  if (!this.archiveDir || !(0, import_fs2.existsSync)(this.archiveDir)) {
@@ -2291,40 +2658,56 @@ var init_trace = __esm({
2291
2658
  } catch {
2292
2659
  }
2293
2660
  }
2294
- persistToDisk() {
2661
+ schedulePersistToDisk() {
2662
+ if (!this.persistEnabled || !this.persistFile) {
2663
+ return;
2664
+ }
2665
+ if (this.persistTimer) {
2666
+ return;
2667
+ }
2668
+ this.persistTimer = setTimeout(() => {
2669
+ this.persistTimer = void 0;
2670
+ this.enqueuePersistToDisk();
2671
+ }, this.persistDebounceMs);
2672
+ this.persistTimer.unref?.();
2673
+ }
2674
+ enqueuePersistToDisk() {
2675
+ this.persistQueue = this.persistQueue.then(() => this.persistToDisk()).catch(() => void 0);
2676
+ }
2677
+ async persistToDisk() {
2295
2678
  if (!this.persistEnabled || !this.persistFile) {
2296
2679
  return;
2297
2680
  }
2298
2681
  try {
2299
- (0, import_fs2.mkdirSync)((0, import_path3.dirname)(this.persistFile), { recursive: true });
2682
+ await (0, import_promises2.mkdir)((0, import_path3.dirname)(this.persistFile), { recursive: true });
2300
2683
  const traces = Array.from(this.cache.values()).sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
2301
2684
  const activeTraces = traces.slice(0, this.activePersistLimit);
2302
2685
  const archivedTraces = traces.slice(this.activePersistLimit);
2303
2686
  if (archivedTraces.length > 0 && this.archiveDir) {
2304
- this.writeArchive(archivedTraces);
2687
+ await this.writeArchive(archivedTraces);
2305
2688
  this.pruneArchives();
2306
2689
  this.cache.clear();
2307
2690
  for (const trace of activeTraces) {
2308
2691
  this.cache.set(trace.requestId, { ...trace, routeReason: [...trace.routeReason] });
2309
2692
  }
2310
2693
  }
2311
- (0, import_fs2.writeFileSync)(this.persistFile, JSON.stringify(activeTraces, null, 2), "utf-8");
2694
+ await (0, import_promises2.writeFile)(this.persistFile, JSON.stringify(activeTraces, null, 2), "utf-8");
2312
2695
  } catch {
2313
2696
  }
2314
2697
  }
2315
- writeArchive(traces) {
2698
+ async writeArchive(traces) {
2316
2699
  if (!this.archiveDir || traces.length === 0) {
2317
2700
  return;
2318
2701
  }
2319
- (0, import_fs2.mkdirSync)(this.archiveDir, { recursive: true });
2702
+ await (0, import_promises2.mkdir)(this.archiveDir, { recursive: true });
2320
2703
  const filename = this.compressArchives ? `governance-traces-${Date.now()}.json.gz` : `governance-traces-${Date.now()}.json`;
2321
2704
  const filePath = (0, import_path3.join)(this.archiveDir, filename);
2322
2705
  const content = JSON.stringify(traces, null, 2);
2323
2706
  if (this.compressArchives) {
2324
- (0, import_fs2.writeFileSync)(filePath, (0, import_zlib.gzipSync)(Buffer.from(content, "utf-8")));
2707
+ await (0, import_promises2.writeFile)(filePath, (0, import_zlib.gzipSync)(Buffer.from(content, "utf-8")));
2325
2708
  return;
2326
2709
  }
2327
- (0, import_fs2.writeFileSync)(filePath, content, "utf-8");
2710
+ await (0, import_promises2.writeFile)(filePath, content, "utf-8");
2328
2711
  }
2329
2712
  readArchiveRecord(file) {
2330
2713
  if (!this.archiveDir) {
@@ -3147,6 +3530,72 @@ If no issue is found, return:
3147
3530
  });
3148
3531
 
3149
3532
  // src/governance/response-governance.ts
3533
+ function hasUpstreamError(payload) {
3534
+ return Boolean(payload && typeof payload === "object" && payload.error);
3535
+ }
3536
+ function describeUpstreamError(payload) {
3537
+ const error = payload?.error;
3538
+ if (typeof error === "string") {
3539
+ return error;
3540
+ }
3541
+ if (typeof error?.message === "string") {
3542
+ return error.message;
3543
+ }
3544
+ if (typeof error?.type === "string") {
3545
+ return error.type;
3546
+ }
3547
+ return "upstream_error";
3548
+ }
3549
+ function getLoopbackApiKey(req, config) {
3550
+ return extractApiKeyFromHeaders(req.headers) || config.APIKEY;
3551
+ }
3552
+ function getModelPoolFallbackAttempt(req) {
3553
+ const attempt = Number(req.body?.metadata?.ctr_model_pool_fallback_attempt ?? 0);
3554
+ return Number.isFinite(attempt) && attempt > 0 ? attempt : 0;
3555
+ }
3556
+ function shouldRecordModelPoolSuccess(req, originalPayload, finalPayload) {
3557
+ if (!req.modelPoolSelection || hasUpstreamError(finalPayload)) {
3558
+ return false;
3559
+ }
3560
+ const routeReason = req.governanceTrace?.routeReason ?? [];
3561
+ if (routeReason.includes("cascade_retry_executed") || routeReason.includes("shadow_sync_guard")) {
3562
+ return false;
3563
+ }
3564
+ if (!hasUpstreamError(originalPayload)) {
3565
+ return true;
3566
+ }
3567
+ return routeReason.includes("model_pool_fallback_executed");
3568
+ }
3569
+ async function executeModelPoolFallbackRetry(requestBody, fallbackModelRef, port, apiKey, timeoutMs, fetchFn) {
3570
+ const fetchImpl = fetchFn || fetch;
3571
+ const currentAttempt = Number(requestBody?.metadata?.ctr_model_pool_fallback_attempt ?? 0);
3572
+ const retryBody = {
3573
+ ...requestBody,
3574
+ model: fallbackModelRef,
3575
+ metadata: {
3576
+ ...requestBody?.metadata ?? {},
3577
+ ctr_model_pool_fallback_attempt: currentAttempt + 1
3578
+ }
3579
+ };
3580
+ try {
3581
+ const response = await fetchImpl(`http://127.0.0.1:${port}/v1/messages`, {
3582
+ method: "POST",
3583
+ headers: {
3584
+ "Content-Type": "application/json",
3585
+ "x-ctr-smart-router": "1",
3586
+ ...apiKey ? { "x-api-key": apiKey } : {}
3587
+ },
3588
+ body: JSON.stringify(retryBody),
3589
+ ...timeoutMs && timeoutMs > 0 ? { signal: AbortSignal.timeout(timeoutMs) } : {}
3590
+ });
3591
+ if (!response.ok) {
3592
+ return null;
3593
+ }
3594
+ return await response.json();
3595
+ } catch {
3596
+ return null;
3597
+ }
3598
+ }
3150
3599
  async function applyResponseGovernance({
3151
3600
  req,
3152
3601
  payload,
@@ -3174,6 +3623,48 @@ async function applyResponseGovernance({
3174
3623
  const detectFailureEvidenceFn = deps?.detectFailureEvidence ?? detectFailureEvidence;
3175
3624
  const decideCascadeEscalationFn = deps?.decideCascadeEscalation ?? decideCascadeEscalation;
3176
3625
  const executeCascadeRetryFn = deps?.executeCascadeRetry ?? executeCascadeRetry;
3626
+ const executeModelPoolFallbackRetryFn = deps?.executeModelPoolFallbackRetry ?? executeModelPoolFallbackRetry;
3627
+ const loopbackApiKey = getLoopbackApiKey(req, config);
3628
+ if (hasUpstreamError(nextPayload) && req.modelPoolSelection && req.governanceTrace) {
3629
+ const fallbackAttempt = getModelPoolFallbackAttempt(req);
3630
+ modelPoolHealthStore.recordFailure(req.modelPoolSelection.modelId, req.modelPoolSelection.endpointId);
3631
+ const fallback = getModelPoolFallbackCandidate(config, req.modelPoolSelection);
3632
+ req.governanceTrace.modelPoolFallbackEvidence = describeUpstreamError(nextPayload);
3633
+ if (fallback && fallbackAttempt < 1) {
3634
+ req.governanceTrace.modelPoolFallbackTriggered = true;
3635
+ req.governanceTrace.modelPoolFallbackFromEndpoint = req.modelPoolSelection.endpointId;
3636
+ req.governanceTrace.modelPoolFallbackNextEndpoint = fallback.endpointId;
3637
+ appendTraceReason(
3638
+ req.governanceTrace,
3639
+ `model_pool_fallback:${fallback.modelId}:${fallback.endpointId}`
3640
+ );
3641
+ const retriedPayload = await executeModelPoolFallbackRetryFn(
3642
+ req.body,
3643
+ fallback.legacyRef,
3644
+ servicePort,
3645
+ loopbackApiKey,
3646
+ config.API_TIMEOUT_MS
3647
+ );
3648
+ if (retriedPayload && !hasUpstreamError(retriedPayload)) {
3649
+ req.body.model = fallback.legacyRef;
3650
+ req.modelPoolSelection = {
3651
+ modelId: fallback.modelId,
3652
+ endpointId: fallback.endpointId,
3653
+ strategy: fallback.strategy
3654
+ };
3655
+ appendTraceReason(req.governanceTrace, "model_pool_fallback_executed");
3656
+ nextPayload = retriedPayload;
3657
+ } else {
3658
+ modelPoolHealthStore.recordFailure(fallback.modelId, fallback.endpointId);
3659
+ appendTraceReason(req.governanceTrace, "model_pool_fallback_failed");
3660
+ if (retriedPayload) {
3661
+ nextPayload = retriedPayload;
3662
+ }
3663
+ }
3664
+ } else if (fallbackAttempt >= 1) {
3665
+ appendTraceReason(req.governanceTrace, "model_pool_fallback_skipped:max_attempts");
3666
+ }
3667
+ }
3177
3668
  if (config.Governance?.enabled && resolvedCascadeConfig?.enabled && req.governanceTrace) {
3178
3669
  const evidences = detectFailureEvidenceFn(nextPayload, resolvedCascadeConfig);
3179
3670
  if (evidences.length > 0) {
@@ -3194,7 +3685,7 @@ async function applyResponseGovernance({
3194
3685
  req.body,
3195
3686
  decision.nextModel,
3196
3687
  servicePort,
3197
- config.APIKEY,
3688
+ loopbackApiKey,
3198
3689
  config.API_TIMEOUT_MS
3199
3690
  );
3200
3691
  if (retriedPayload) {
@@ -3222,7 +3713,7 @@ async function applyResponseGovernance({
3222
3713
  resolvedShadowConfig,
3223
3714
  servicePort,
3224
3715
  void 0,
3225
- config.APIKEY,
3716
+ loopbackApiKey,
3226
3717
  config.API_TIMEOUT_MS
3227
3718
  ) : shadowSupervisor.inspect(nextPayload, resolvedShadowConfig);
3228
3719
  if (audit.triggered) {
@@ -3242,7 +3733,7 @@ async function applyResponseGovernance({
3242
3733
  req.body,
3243
3734
  guardDecision.nextModel,
3244
3735
  servicePort,
3245
- config.APIKEY,
3736
+ loopbackApiKey,
3246
3737
  config.API_TIMEOUT_MS
3247
3738
  );
3248
3739
  if (guardedPayload) {
@@ -3259,6 +3750,14 @@ async function applyResponseGovernance({
3259
3750
  req.governanceTrace = finalizeTrace(req.governanceTrace, {
3260
3751
  finalModel: req.body?.model ?? req.governanceTrace.finalModel
3261
3752
  });
3753
+ if (shouldRecordModelPoolSuccess(req, payload, nextPayload)) {
3754
+ modelPoolHealthStore.recordSuccess(
3755
+ req.modelPoolSelection.modelId,
3756
+ req.modelPoolSelection.endpointId,
3757
+ req.governanceTrace.completedAt ?? Date.now(),
3758
+ req.governanceTrace.latencyMs
3759
+ );
3760
+ }
3262
3761
  recordGovernanceTrace(req.governanceTrace);
3263
3762
  }
3264
3763
  return nextPayload;
@@ -3271,6 +3770,8 @@ var init_response_governance = __esm({
3271
3770
  init_cascade_gate();
3272
3771
  init_shadow_supervisor();
3273
3772
  init_compile();
3773
+ init_api_keys();
3774
+ init_pool_health();
3274
3775
  }
3275
3776
  });
3276
3777
 
@@ -3592,6 +4093,237 @@ function isRoutedTrace(trace) {
3592
4093
  function isModelSwitch(trace) {
3593
4094
  return Boolean(trace.initialModel && trace.finalModel && trace.initialModel !== trace.finalModel);
3594
4095
  }
4096
+ function hasRouteReasonPrefix(trace, prefix) {
4097
+ return trace.routeReason.some((reason) => reason === prefix || reason.startsWith(`${prefix}:`));
4098
+ }
4099
+ function compactCsvEvidence(value) {
4100
+ return value.replace(/[\r\n]+/g, " ").replace(/,/g, ";");
4101
+ }
4102
+ function classifyVerificationResult(value) {
4103
+ const normalized = value.toLowerCase();
4104
+ if (/\b(pass|passed|ok|clean|approved)\b/.test(normalized) || /no\s+(risk|issue|violation|error|failure|fail)/.test(normalized)) {
4105
+ return "info";
4106
+ }
4107
+ return /fail|risk|unsafe|violation|missing|placeholder|error/.test(normalized) ? "warn" : "info";
4108
+ }
4109
+ function getTaskComparisonKey(trace) {
4110
+ if (trace.semanticIntent) {
4111
+ return trace.semanticIntent;
4112
+ }
4113
+ const semanticReason = trace.routeReason.find(
4114
+ (reason) => reason.startsWith("semantic_match:") || reason.startsWith("semantic:intent:")
4115
+ );
4116
+ if (semanticReason) {
4117
+ if (semanticReason.startsWith("semantic:intent:")) {
4118
+ return semanticReason.slice("semantic:intent:".length);
4119
+ }
4120
+ return semanticReason.slice("semantic_match:".length);
4121
+ }
4122
+ return void 0;
4123
+ }
4124
+ function isTraceFailure(trace) {
4125
+ return Boolean(
4126
+ trace.cascadeTriggered || (trace.cascadeEvidence?.length ?? 0) > 0 || trace.modelPoolFallbackTriggered || hasRouteReasonPrefix(trace, "context_window_exceeded") || trace.verificationResult && classifyVerificationResult(trace.verificationResult) === "warn"
4127
+ );
4128
+ }
4129
+ function buildTaskComparisonSummary(traces, limit = 5) {
4130
+ const tasks = {};
4131
+ for (const trace of traces) {
4132
+ const taskKey = getTaskComparisonKey(trace);
4133
+ const model = trace.finalModel;
4134
+ if (!taskKey || !model) {
4135
+ continue;
4136
+ }
4137
+ tasks[taskKey] ??= {};
4138
+ tasks[taskKey][model] ??= {
4139
+ model,
4140
+ totalTraces: 0,
4141
+ failureCount: 0,
4142
+ alignmentUsedCount: 0,
4143
+ cascadeTriggeredCount: 0,
4144
+ latencyValues: []
4145
+ };
4146
+ const item = tasks[taskKey][model];
4147
+ item.totalTraces += 1;
4148
+ item.failureCount += isTraceFailure(trace) ? 1 : 0;
4149
+ item.alignmentUsedCount += trace.alignmentUsed ? 1 : 0;
4150
+ item.cascadeTriggeredCount += trace.cascadeTriggered ? 1 : 0;
4151
+ if (typeof trace.latencyMs === "number") {
4152
+ item.latencyValues.push(trace.latencyMs);
4153
+ }
4154
+ }
4155
+ const comparisons = Object.entries(tasks).map(([taskKey, modelMap]) => {
4156
+ const models = Object.values(modelMap).map((model) => ({
4157
+ model: model.model,
4158
+ totalTraces: model.totalTraces,
4159
+ failureCount: model.failureCount,
4160
+ failureRate: rate(model.failureCount, model.totalTraces),
4161
+ latencySampleCount: model.latencyValues.length,
4162
+ averageLatencyMs: average(model.latencyValues),
4163
+ alignmentUsedRate: rate(model.alignmentUsedCount, model.totalTraces),
4164
+ cascadeTriggeredRate: rate(model.cascadeTriggeredCount, model.totalTraces)
4165
+ })).sort((left, right) => {
4166
+ if (left.failureRate !== right.failureRate) {
4167
+ return left.failureRate - right.failureRate;
4168
+ }
4169
+ if (Boolean(right.latencySampleCount) !== Boolean(left.latencySampleCount)) {
4170
+ return right.latencySampleCount - left.latencySampleCount;
4171
+ }
4172
+ if (left.averageLatencyMs !== right.averageLatencyMs) {
4173
+ return left.averageLatencyMs - right.averageLatencyMs;
4174
+ }
4175
+ return right.totalTraces - left.totalTraces;
4176
+ });
4177
+ const modelCount = models.length;
4178
+ const totalTraces = models.reduce((sum, model) => sum + model.totalTraces, 0);
4179
+ if (modelCount < 2 || totalTraces < 2) {
4180
+ return void 0;
4181
+ }
4182
+ const baseline = [...models].sort((left, right) => {
4183
+ if (right.totalTraces !== left.totalTraces) {
4184
+ return right.totalTraces - left.totalTraces;
4185
+ }
4186
+ return left.model.localeCompare(right.model);
4187
+ })[0];
4188
+ const best = models[0];
4189
+ const latencyModels = models.filter((model) => model.latencySampleCount > 0);
4190
+ const fastest = [...latencyModels.length ? latencyModels : models].sort((left, right) => {
4191
+ if (left.averageLatencyMs !== right.averageLatencyMs) {
4192
+ return left.averageLatencyMs - right.averageLatencyMs;
4193
+ }
4194
+ return left.failureRate - right.failureRate;
4195
+ })[0];
4196
+ const latencyDeltaMs = baseline.latencySampleCount > 0 && fastest.latencySampleCount > 0 ? Number((baseline.averageLatencyMs - fastest.averageLatencyMs).toFixed(2)) : 0;
4197
+ return {
4198
+ taskKey,
4199
+ totalTraces,
4200
+ modelCount,
4201
+ baselineModel: baseline.model,
4202
+ bestModel: best.model,
4203
+ fastestModel: fastest.model,
4204
+ failureRateDelta: Number((baseline.failureRate - best.failureRate).toFixed(4)),
4205
+ latencyDeltaMs,
4206
+ models
4207
+ };
4208
+ }).filter((item) => Boolean(item)).sort((left, right) => {
4209
+ if (right.failureRateDelta !== left.failureRateDelta) {
4210
+ return right.failureRateDelta - left.failureRateDelta;
4211
+ }
4212
+ if (right.latencyDeltaMs !== left.latencyDeltaMs) {
4213
+ return right.latencyDeltaMs - left.latencyDeltaMs;
4214
+ }
4215
+ return right.totalTraces - left.totalTraces;
4216
+ });
4217
+ return {
4218
+ totalComparedTasks: comparisons.length,
4219
+ totalComparedTraces: comparisons.reduce((sum, item) => sum + item.totalTraces, 0),
4220
+ bestQualityLiftTask: comparisons.find((item) => item.failureRateDelta > 0),
4221
+ bestSpeedLiftTask: [...comparisons].sort((left, right) => right.latencyDeltaMs - left.latencyDeltaMs).find((item) => item.latencyDeltaMs > 0),
4222
+ comparisons: comparisons.slice(0, limit)
4223
+ };
4224
+ }
4225
+ function buildQualityEvidenceSummary(traces, thresholds, limit = 8) {
4226
+ const samples = [];
4227
+ const distribution = {};
4228
+ const addSample = (trace, type, severity, evidence, action) => {
4229
+ distribution[type] = (distribution[type] ?? 0) + 1;
4230
+ samples.push({
4231
+ requestId: trace.requestId,
4232
+ type,
4233
+ severity,
4234
+ evidence,
4235
+ action,
4236
+ routeReason: [...trace.routeReason],
4237
+ initialModel: trace.initialModel,
4238
+ finalModel: trace.finalModel,
4239
+ semanticIntent: trace.semanticIntent,
4240
+ latencyMs: trace.latencyMs,
4241
+ startedAt: trace.startedAt
4242
+ });
4243
+ };
4244
+ for (const trace of traces) {
4245
+ if (trace.cascadeTriggered || (trace.cascadeEvidence?.length ?? 0) > 0) {
4246
+ addSample(
4247
+ trace,
4248
+ "cascade_failure",
4249
+ trace.cascadeTriggered ? "critical" : "warn",
4250
+ trace.cascadeEvidence?.length ? trace.cascadeEvidence.join("; ") : "Cascade retry was triggered.",
4251
+ "Review cascade evidence and compare the retry model output with the original model."
4252
+ );
4253
+ }
4254
+ if (trace.modelPoolFallbackTriggered) {
4255
+ addSample(
4256
+ trace,
4257
+ "model_pool_fallback",
4258
+ "warn",
4259
+ trace.modelPoolFallbackEvidence || `${trace.modelPoolFallbackFromEndpoint ?? "-"} -> ${trace.modelPoolFallbackNextEndpoint ?? "-"}`,
4260
+ "Inspect model pool endpoint health before sending more traffic to this pool."
4261
+ );
4262
+ }
4263
+ if (hasRouteReasonPrefix(trace, "context_window_exceeded")) {
4264
+ addSample(
4265
+ trace,
4266
+ "context_window_guard",
4267
+ "critical",
4268
+ trace.routeReason.find((reason) => reason.startsWith("context_window_exceeded")) || "context window exceeded",
4269
+ "Add model context metadata or route this task class to a larger context model."
4270
+ );
4271
+ } else if (hasRouteReasonPrefix(trace, "context_window_fallback")) {
4272
+ addSample(
4273
+ trace,
4274
+ "context_window_guard",
4275
+ "info",
4276
+ trace.routeReason.find((reason) => reason.startsWith("context_window_fallback")) || "long-context fallback used",
4277
+ "Keep this as positive evidence that long-context fallback protected the request."
4278
+ );
4279
+ }
4280
+ if (trace.shadowChecked && trace.verificationResult) {
4281
+ const severity = classifyVerificationResult(trace.verificationResult);
4282
+ addSample(
4283
+ trace,
4284
+ "shadow_verification",
4285
+ severity,
4286
+ trace.verificationResult,
4287
+ severity === "warn" ? "Review verifier findings before widening this route." : "Keep verifier pass as quality evidence for this route."
4288
+ );
4289
+ }
4290
+ if (typeof trace.latencyMs === "number" && trace.latencyMs >= thresholds.latencyWarnMs) {
4291
+ addSample(
4292
+ trace,
4293
+ "slow_request",
4294
+ trace.latencyMs >= thresholds.latencyCriticalMs ? "critical" : "warn",
4295
+ `latencyMs=${trace.latencyMs}`,
4296
+ "Compare this route with faster candidates before making it default traffic."
4297
+ );
4298
+ }
4299
+ if (isModelSwitch(trace) && trace.alignmentUsed) {
4300
+ addSample(
4301
+ trace,
4302
+ "alignment_continuity",
4303
+ "info",
4304
+ `${trace.initialModel ?? "-"} -> ${trace.finalModel ?? "-"} with context alignment`,
4305
+ "Keep this as continuity evidence for model switching."
4306
+ );
4307
+ }
4308
+ }
4309
+ const severityRank = { critical: 0, warn: 1, info: 2 };
4310
+ const rankedSamples = samples.sort((left, right) => {
4311
+ if (severityRank[left.severity] !== severityRank[right.severity]) {
4312
+ return severityRank[left.severity] - severityRank[right.severity];
4313
+ }
4314
+ return right.startedAt - left.startedAt;
4315
+ }).slice(0, limit);
4316
+ return {
4317
+ totalSamples: samples.length,
4318
+ failureSamples: samples.filter((sample) => sample.severity !== "info").length,
4319
+ improvementSamples: samples.filter(
4320
+ (sample) => sample.type === "alignment_continuity" || sample.type === "context_window_guard" && sample.severity === "info" || sample.type === "shadow_verification" && sample.severity === "info"
4321
+ ).length,
4322
+ speedRiskSamples: samples.filter((sample) => sample.type === "slow_request").length,
4323
+ byType: buildTopEntries(distribution, samples.length, 8),
4324
+ samples: rankedSamples
4325
+ };
4326
+ }
3595
4327
  function summarizeRoutingOutcomes(traces) {
3596
4328
  const routedTraces = traces.filter(isRoutedTrace);
3597
4329
  const switchedTraces = traces.filter(isModelSwitch);
@@ -3600,6 +4332,8 @@ function summarizeRoutingOutcomes(traces) {
3600
4332
  ).length;
3601
4333
  const alignmentOnSwitchCount = switchedTraces.filter((trace) => trace.alignmentUsed).length;
3602
4334
  const cascadeAfterSwitchCount = switchedTraces.filter((trace) => trace.cascadeTriggered).length;
4335
+ const contextWindowFallbackCount = traces.filter((trace) => hasRouteReasonPrefix(trace, "context_window_fallback")).length;
4336
+ const contextWindowExceededCount = traces.filter((trace) => hasRouteReasonPrefix(trace, "context_window_exceeded")).length;
3603
4337
  const switchDistribution = {};
3604
4338
  const routeLatencyValues = {};
3605
4339
  const routeReasonGroups = {};
@@ -3640,6 +4374,10 @@ function summarizeRoutingOutcomes(traces) {
3640
4374
  alignmentOnSwitchRate: rate(alignmentOnSwitchCount, switchedTraces.length),
3641
4375
  cascadeAfterSwitchCount,
3642
4376
  cascadeAfterSwitchRate: rate(cascadeAfterSwitchCount, switchedTraces.length),
4377
+ contextWindowFallbackCount,
4378
+ contextWindowFallbackRate: rate(contextWindowFallbackCount, traces.length),
4379
+ contextWindowExceededCount,
4380
+ contextWindowExceededRate: rate(contextWindowExceededCount, traces.length),
3643
4381
  averageLatencyByRouteReason,
3644
4382
  topModelSwitches: buildTopSwitchEntries(switchDistribution, switchedTraces.length),
3645
4383
  byRouteReason: buildOutcomeGroupEntries(routeReasonGroups, traces.length),
@@ -3735,12 +4473,111 @@ function buildHealthActions(anomalies) {
3735
4473
  }
3736
4474
  return Array.from(actions);
3737
4475
  }
4476
+ function percent(value) {
4477
+ return `${Number((value * 100).toFixed(1))}%`;
4478
+ }
4479
+ function topOutcomeGroup(groups, predicate) {
4480
+ return groups.filter(predicate).sort((left, right) => {
4481
+ const leftScore = left.cascadeAfterSwitchRate + left.modelSwitchRate;
4482
+ const rightScore = right.cascadeAfterSwitchRate + right.modelSwitchRate;
4483
+ if (rightScore !== leftScore) {
4484
+ return rightScore - leftScore;
4485
+ }
4486
+ if (right.totalTraces !== left.totalTraces) {
4487
+ return right.totalTraces - left.totalTraces;
4488
+ }
4489
+ return left.key.localeCompare(right.key);
4490
+ })[0];
4491
+ }
4492
+ function topSlowOutcomeGroup(groups) {
4493
+ return groups.filter((group) => group.averageLatencyMs >= DEFAULT_ANOMALY_THRESHOLDS.latencyWarnMs).sort((left, right) => {
4494
+ if (right.averageLatencyMs !== left.averageLatencyMs) {
4495
+ return right.averageLatencyMs - left.averageLatencyMs;
4496
+ }
4497
+ if (right.totalTraces !== left.totalTraces) {
4498
+ return right.totalTraces - left.totalTraces;
4499
+ }
4500
+ return left.key.localeCompare(right.key);
4501
+ })[0];
4502
+ }
4503
+ function buildRoutingTuningRecommendations(metrics, outcome) {
4504
+ if (!outcome || metrics.totalTraces === 0) {
4505
+ return [];
4506
+ }
4507
+ const recommendations = [];
4508
+ if (outcome.routedTraces < DEFAULT_ANOMALY_THRESHOLDS.minSampleSize) {
4509
+ recommendations.push({
4510
+ code: "collect_routing_samples",
4511
+ severity: "info",
4512
+ message: "Routing sample size is still small.",
4513
+ evidence: `routedTraces=${outcome.routedTraces}`,
4514
+ action: "Collect at least 3 routed traces before changing routing policy."
4515
+ });
4516
+ }
4517
+ if (outcome.contextWindowExceededCount > 0) {
4518
+ recommendations.push({
4519
+ code: "context_window_exceeded",
4520
+ severity: "critical",
4521
+ message: "Some requests exceeded the selected model context window.",
4522
+ evidence: `contextWindowExceededRate=${percent(outcome.contextWindowExceededRate)}`,
4523
+ action: "Review model context window metadata and Router.longContext coverage."
4524
+ });
4525
+ } else if (outcome.contextWindowFallbackRate >= 0.3) {
4526
+ recommendations.push({
4527
+ code: "context_window_fallback_high",
4528
+ severity: outcome.contextWindowFallbackRate >= 0.6 ? "warn" : "info",
4529
+ message: "Long-context fallback is frequent enough to affect latency planning.",
4530
+ evidence: `contextWindowFallbackRate=${percent(outcome.contextWindowFallbackRate)}`,
4531
+ action: "Monitor context window fallback rate and long-context model latency."
4532
+ });
4533
+ }
4534
+ const switchWithoutAlignment = topOutcomeGroup(
4535
+ outcome.byRouteReason,
4536
+ (group) => group.modelSwitchCount > 0 && group.modelSwitchRate >= 0.5 && group.alignmentOnSwitchRate < 0.5
4537
+ );
4538
+ if (switchWithoutAlignment) {
4539
+ recommendations.push({
4540
+ code: "switch_without_alignment",
4541
+ severity: "warn",
4542
+ message: "A high-switch route is not consistently using alignment.",
4543
+ evidence: `${switchWithoutAlignment.key}:switch=${percent(switchWithoutAlignment.modelSwitchRate)}:alignment=${percent(switchWithoutAlignment.alignmentOnSwitchRate)}`,
4544
+ action: "Enable or tune SmartRouter sticky alignment for high-switch routes."
4545
+ });
4546
+ }
4547
+ const cascadeAfterSwitch = topOutcomeGroup(
4548
+ outcome.byRouteReason,
4549
+ (group) => group.cascadeAfterSwitchCount > 0 && group.cascadeAfterSwitchRate >= DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate
4550
+ );
4551
+ if (cascadeAfterSwitch || outcome.cascadeAfterSwitchRate >= DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate) {
4552
+ const cascadeRate = cascadeAfterSwitch?.cascadeAfterSwitchRate ?? outcome.cascadeAfterSwitchRate;
4553
+ const severity = cascadeRate >= DEFAULT_ANOMALY_THRESHOLDS.cascadeCriticalRate ? "critical" : "warn";
4554
+ recommendations.push({
4555
+ code: "switch_cascade_risk",
4556
+ severity,
4557
+ message: "Model switches are followed by cascade retries often enough to review policy.",
4558
+ evidence: cascadeAfterSwitch ? `${cascadeAfterSwitch.key}:cascadeAfterSwitch=${percent(cascadeAfterSwitch.cascadeAfterSwitchRate)}` : `cascadeAfterSwitchRate=${percent(outcome.cascadeAfterSwitchRate)}`,
4559
+ action: "Review high-cascade route groups before widening SmartRouter candidates."
4560
+ });
4561
+ }
4562
+ const slowRoute = topSlowOutcomeGroup(outcome.byRouteReason);
4563
+ if (slowRoute) {
4564
+ recommendations.push({
4565
+ code: "slow_route_group",
4566
+ severity: slowRoute.averageLatencyMs >= DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs ? "critical" : "warn",
4567
+ message: "A route group is slower than the governance latency warning threshold.",
4568
+ evidence: `${slowRoute.key}:averageLatencyMs=${slowRoute.averageLatencyMs}`,
4569
+ action: "Inspect slow route groups before making them default traffic."
4570
+ });
4571
+ }
4572
+ return recommendations.slice(0, 5);
4573
+ }
3738
4574
  function buildGovernanceHealthSummary(input3) {
3739
4575
  const metrics = input3.metrics;
3740
4576
  const anomalies = input3.anomalies ?? [];
3741
4577
  const criticalCount = anomalies.filter((item) => item.severity === "critical").length;
3742
4578
  const warnCount = anomalies.filter((item) => item.severity === "warn").length;
3743
4579
  const alertCount = anomalies.length;
4580
+ const routingTuning = buildRoutingTuningRecommendations(metrics, input3.outcome);
3744
4581
  if (metrics.totalTraces === 0) {
3745
4582
  return {
3746
4583
  status: "idle",
@@ -3756,16 +4593,33 @@ function buildGovernanceHealthSummary(input3) {
3756
4593
  alignmentUsedRate: 0,
3757
4594
  modelSwitchRate: 0,
3758
4595
  alignmentOnSwitchRate: 0,
4596
+ contextWindowFallbackRate: 0,
4597
+ contextWindowExceededRate: 0,
3759
4598
  averageLatencyMs: 0,
3760
4599
  topRouteReason: input3.topRouteReasons?.[0],
3761
4600
  topFinalModel: input3.topFinalModels?.[0]
3762
4601
  },
3763
- actions: ["Send requests through the router to collect governance traces."]
4602
+ actions: ["Send requests through the router to collect governance traces."],
4603
+ routingTuning: []
3764
4604
  };
3765
4605
  }
3766
4606
  const status = criticalCount > 0 ? "critical" : warnCount > 0 ? "watch" : "healthy";
3767
4607
  const alertVerb = alertCount === 1 ? "needs" : "need";
3768
4608
  const message = status === "healthy" ? `Healthy over ${metrics.totalTraces} traces.` : `${alertCount} governance alert${alertCount === 1 ? "" : "s"} ${alertVerb} attention (${criticalCount} critical / ${warnCount} warning${warnCount === 1 ? "" : "s"}).`;
4609
+ const actions = new Set(buildHealthActions(anomalies));
4610
+ if (!anomalies.length && routingTuning.some((item) => item.severity !== "info")) {
4611
+ actions.delete("Continue monitoring route and model distributions.");
4612
+ }
4613
+ if ((input3.outcome?.contextWindowExceededCount ?? 0) > 0) {
4614
+ actions.add("Review model context window metadata and Router.longContext coverage.");
4615
+ } else if ((input3.outcome?.contextWindowFallbackCount ?? 0) > 0) {
4616
+ actions.add("Monitor context window fallback rate and long-context model latency.");
4617
+ }
4618
+ for (const recommendation of routingTuning) {
4619
+ if (recommendation.severity !== "info") {
4620
+ actions.add(recommendation.action);
4621
+ }
4622
+ }
3769
4623
  return {
3770
4624
  status,
3771
4625
  message,
@@ -3780,11 +4634,14 @@ function buildGovernanceHealthSummary(input3) {
3780
4634
  alignmentUsedRate: metrics.alignmentUsedRate,
3781
4635
  modelSwitchRate: input3.outcome?.modelSwitchRate ?? 0,
3782
4636
  alignmentOnSwitchRate: input3.outcome?.alignmentOnSwitchRate ?? 0,
4637
+ contextWindowFallbackRate: input3.outcome?.contextWindowFallbackRate ?? 0,
4638
+ contextWindowExceededRate: input3.outcome?.contextWindowExceededRate ?? 0,
3783
4639
  averageLatencyMs: metrics.averageLatencyMs,
3784
4640
  topRouteReason: input3.topRouteReasons?.[0],
3785
4641
  topFinalModel: input3.topFinalModels?.[0]
3786
4642
  },
3787
- actions: buildHealthActions(anomalies)
4643
+ actions: Array.from(actions),
4644
+ routingTuning
3788
4645
  };
3789
4646
  }
3790
4647
  function summarizeGovernanceMetrics(traces) {
@@ -3883,6 +4740,8 @@ function getGovernanceMetricsReport(options = {}) {
3883
4740
  const topFinalModels = buildTopEntries(metrics.finalModelDistribution, limitedTraces.length);
3884
4741
  const topSemanticIntents = buildTopEntries(metrics.semanticIntentDistribution, limitedTraces.length);
3885
4742
  const anomalies = buildAnomalies(metrics, buckets, thresholds);
4743
+ const qualityEvidence = buildQualityEvidenceSummary(limitedTraces, thresholds);
4744
+ const taskComparison = buildTaskComparisonSummary(limitedTraces);
3886
4745
  return {
3887
4746
  windowMs: options.windowMs,
3888
4747
  bucketCount,
@@ -3895,6 +4754,8 @@ function getGovernanceMetricsReport(options = {}) {
3895
4754
  topFinalModels,
3896
4755
  topSemanticIntents,
3897
4756
  anomalies,
4757
+ qualityEvidence,
4758
+ taskComparison,
3898
4759
  health: buildGovernanceHealthSummary({
3899
4760
  metrics,
3900
4761
  anomalies,
@@ -3920,11 +4781,32 @@ function exportGovernanceMetricsReport(report, format = "json") {
3920
4781
  `outcome,routedRate,${report.outcome.routedRate}`,
3921
4782
  `outcome,modelSwitchRate,${report.outcome.modelSwitchRate}`,
3922
4783
  `outcome,alignmentOnSwitchRate,${report.outcome.alignmentOnSwitchRate}`,
3923
- `outcome,cascadeAfterSwitchRate,${report.outcome.cascadeAfterSwitchRate}`
4784
+ `outcome,cascadeAfterSwitchRate,${report.outcome.cascadeAfterSwitchRate}`,
4785
+ `outcome,contextWindowFallbackRate,${report.outcome.contextWindowFallbackRate}`,
4786
+ `outcome,contextWindowExceededRate,${report.outcome.contextWindowExceededRate}`
3924
4787
  ];
3925
4788
  if (report.health) {
3926
4789
  lines.push(`summary,healthStatus,${report.health.status}`);
3927
4790
  lines.push(`summary,healthMessage,${report.health.message}`);
4791
+ for (const item of report.health.routingTuning ?? []) {
4792
+ lines.push(`routingTuning,${item.code},${item.severity}:${item.evidence}`);
4793
+ }
4794
+ }
4795
+ if (report.qualityEvidence) {
4796
+ lines.push(`qualityEvidence,totalSamples,${report.qualityEvidence.totalSamples}`);
4797
+ lines.push(`qualityEvidence,failureSamples,${report.qualityEvidence.failureSamples}`);
4798
+ lines.push(`qualityEvidence,improvementSamples,${report.qualityEvidence.improvementSamples}`);
4799
+ lines.push(`qualityEvidence,speedRiskSamples,${report.qualityEvidence.speedRiskSamples}`);
4800
+ for (const item of report.qualityEvidence.samples) {
4801
+ lines.push(`qualityEvidenceSample,${item.type},${item.severity}:${item.requestId}:${compactCsvEvidence(item.evidence)}`);
4802
+ }
4803
+ }
4804
+ if (report.taskComparison) {
4805
+ lines.push(`taskComparison,totalComparedTasks,${report.taskComparison.totalComparedTasks}`);
4806
+ lines.push(`taskComparison,totalComparedTraces,${report.taskComparison.totalComparedTraces}`);
4807
+ for (const item of report.taskComparison.comparisons) {
4808
+ lines.push(`taskComparisonSample,${compactCsvEvidence(item.taskKey)},best=${compactCsvEvidence(item.bestModel)}:baseline=${compactCsvEvidence(item.baselineModel)}:failureRateDelta=${item.failureRateDelta}:latencyDeltaMs=${item.latencyDeltaMs}`);
4809
+ }
3928
4810
  }
3929
4811
  for (const anomaly of report.anomalies) {
3930
4812
  lines.push(`anomaly,${anomaly.type},${anomaly.severity}:${anomaly.value}`);
@@ -4409,7 +5291,7 @@ var init_runtime_role_guidance = __esm({
4409
5291
  "use strict";
4410
5292
  LOCAL_USER_ROLE_GUIDE = "\u672C\u5730\u4F7F\u7528\u8005\uFF1A\u5148\u8DD1\u901A Models + Router.default\uFF0C\u518D\u7528 ctr start / ctr status / ctr code \u8FDB\u5165 Claude Code\u3002";
4411
5293
  SERVER_MAINTAINER_ROLE_GUIDE = "\u670D\u52A1\u7EF4\u62A4\u8005\uFF1A\u7528 ctr deploy init --target server \u751F\u6210 server \u914D\u7F6E\uFF0C\u4FDD\u7559 bootstrap/admin key \u7BA1\u7406\u670D\u52A1\uFF0C\u5E76\u7ED9\u8FDC\u7A0B\u4F7F\u7528\u8005\u53D1\u653E managed client + read-only key\u3002";
4412
- REMOTE_CLIENT_ROLE_GUIDE = "\u8FDC\u7A0B\u4F7F\u7528\u8005\uFF1A\u62FF\u5230\u670D\u52A1\u5730\u5740\u548C managed client + read-only key\uFF1BRuntime.remote_service \u8D1F\u8D23\u8FDE\u63A5\u914D\u7F6E\u4E0E ready/status \u68C0\u67E5\uFF0C\u76F4\u8FDE Claude Code \u65F6\u8BBE\u7F6E ANTHROPIC_BASE_URL \u4E0E ANTHROPIC_AUTH_TOKEN\u3002";
5294
+ REMOTE_CLIENT_ROLE_GUIDE = "\u8FDC\u7A0B\u4F7F\u7528\u8005\uFF1A\u62FF\u5230\u670D\u52A1\u5730\u5740\u548C managed client + read-only key\uFF1BRuntime.remote_service \u8D1F\u8D23 ready/status \u68C0\u67E5\uFF0C\u5E76\u8BA9\u672C\u5730 ctr \u4EE3\u7406\u628A\u6A21\u578B\u8BF7\u6C42\u8F6C\u53D1\u5230\u8FDC\u7A0B\u670D\u52A1\u3002";
4413
5295
  }
4414
5296
  });
4415
5297
 
@@ -4470,7 +5352,7 @@ function renderWorkbenchHtml(rawInitialConfig, configuredThresholds = {}) {
4470
5352
  const escapedCascadeWarnRate = escapeHtml(configuredThresholds.cascade_warn_rate ?? 0.4);
4471
5353
  const escapedShadowWarnRate = escapeHtml(configuredThresholds.shadow_warn_rate ?? 0.5);
4472
5354
  const escapedLatencyWarnMs = escapeHtml(configuredThresholds.latency_warn_ms ?? 1500);
4473
- 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">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\u4E3A priority \u8C03\u5EA6\u5951\u7EA6</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">\u7EF4\u62A4\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001\u91CD\u542F\u3001auth \u7BA1\u7406\u548C\u6CBB\u7406\u5199\u64CD\u4F5C\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\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="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/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>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 securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable 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=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[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 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>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 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(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=>'<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+'</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'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; }}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)], ['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 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); 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>`;
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=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[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>`;
4474
5356
  }
4475
5357
  var init_workbench = __esm({
4476
5358
  "src/ui/workbench.ts"() {
@@ -4613,7 +5495,7 @@ function buildServiceInfo(rawConfig) {
4613
5495
  baseUrl: remoteService.base_url || "",
4614
5496
  authTokenConfigured: Boolean(remoteService.auth_token),
4615
5497
  recommendedScopes: ["client", "read-only"],
4616
- guidance: "Use Runtime.remote_service.base_url and a managed client + read-only key from the server maintainer."
5498
+ guidance: "Local CTR forwards model calls to Runtime.remote_service.base_url with a managed client + read-only key from the server maintainer."
4617
5499
  } : runtimeMode === "local" ? {
4618
5500
  role: "local_user",
4619
5501
  baseUrl: localBaseUrl,
@@ -4678,6 +5560,66 @@ function buildRegistrationInfo(rawConfig) {
4678
5560
  })
4679
5561
  };
4680
5562
  }
5563
+ function buildModelPoolHealthReport(rawConfig) {
5564
+ const normalizedResult = normalizeAndValidateConfig(rawConfig ?? {});
5565
+ const normalized = normalizedResult.config;
5566
+ const registry = buildModelRegistry(normalized);
5567
+ const pools = Object.values(registry.modelPools ?? {}).map((pool) => {
5568
+ const endpoints2 = (pool.endpoints ?? []).map((endpoint) => {
5569
+ const health = modelPoolHealthStore.getSnapshot(pool.modelId, endpoint.id);
5570
+ return {
5571
+ id: endpoint.id,
5572
+ modelId: pool.modelId,
5573
+ providerName: endpoint.providerName,
5574
+ modelName: endpoint.modelName,
5575
+ upstreamServiceId: endpoint.upstreamServiceId,
5576
+ upstreamBaseUrl: endpoint.upstreamBaseUrl,
5577
+ priority: endpoint.priority,
5578
+ enabled: endpoint.enabled,
5579
+ active: endpoint.id === pool.activeEndpointId,
5580
+ status: health.status,
5581
+ failureCount: health.failureCount,
5582
+ successCount: health.successCount,
5583
+ lastFailureAt: health.lastFailureAt,
5584
+ lastSuccessAt: health.lastSuccessAt,
5585
+ cooldownUntil: health.cooldownUntil,
5586
+ circuitOpenUntil: health.circuitOpenUntil,
5587
+ latency: health.latency
5588
+ };
5589
+ });
5590
+ return {
5591
+ modelId: pool.modelId,
5592
+ strategy: pool.strategy,
5593
+ activeEndpointId: pool.activeEndpointId,
5594
+ endpoints: endpoints2,
5595
+ warnings: pool.warnings ?? []
5596
+ };
5597
+ });
5598
+ const endpoints = pools.flatMap((pool) => pool.endpoints);
5599
+ const statusCounts = endpoints.reduce((counts, endpoint) => {
5600
+ counts[endpoint.status] = (counts[endpoint.status] ?? 0) + 1;
5601
+ return counts;
5602
+ }, {});
5603
+ const latencySamples = endpoints.map((endpoint) => endpoint.latency?.averageMs).filter((value) => typeof value === "number" && Number.isFinite(value));
5604
+ const persistence = modelPoolHealthStore.exportForPersistence();
5605
+ return {
5606
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5607
+ persistedState: {
5608
+ updatedAt: persistence.updatedAt,
5609
+ endpoints: persistence.endpoints.length
5610
+ },
5611
+ summary: {
5612
+ pools: pools.length,
5613
+ endpoints: endpoints.length,
5614
+ healthy: statusCounts.healthy ?? 0,
5615
+ cooldown: statusCounts.cooldown ?? 0,
5616
+ open: statusCounts.open ?? 0,
5617
+ averageLatencyMs: latencySamples.length ? latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length : void 0
5618
+ },
5619
+ pools,
5620
+ warnings: normalizedResult.warnings
5621
+ };
5622
+ }
4681
5623
  function summarizeCompiledModels(normalized) {
4682
5624
  const compiled = toCompiledRegistryView(normalized);
4683
5625
  const capabilityWarnings = collectCapabilityWarnings(normalized);
@@ -5019,6 +5961,7 @@ var init_server = __esm({
5019
5961
  init_service_health();
5020
5962
  init_governance();
5021
5963
  init_compile();
5964
+ init_pool_health();
5022
5965
  init_schema();
5023
5966
  init_validation_contract();
5024
5967
  init_workbench();
@@ -5075,6 +6018,9 @@ var init_server = __esm({
5075
6018
  })
5076
6019
  };
5077
6020
  });
6021
+ server.app.get("/api/models/pool-health", async () => {
6022
+ return buildModelPoolHealthReport(config.initialConfig ?? {});
6023
+ });
5078
6024
  server.app.post("/api/models/compiled/preview", async (req, reply) => {
5079
6025
  const rawConfig = req.body ?? {};
5080
6026
  let rawCompiled = null;
@@ -5274,13 +6220,17 @@ var init_server = __esm({
5274
6220
  server.app.get("/api/remote-status", async (req) => {
5275
6221
  const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
5276
6222
  const normalized = normalizedResult.config;
5277
- const remote = await probeRemoteServiceStatus(normalized.Runtime?.remote_service);
6223
+ const [remote, remoteRegistration] = await Promise.all([
6224
+ probeRemoteServiceStatus(normalized.Runtime?.remote_service),
6225
+ probeRemoteRegistrationStatus(normalized.Runtime?.remote_service)
6226
+ ]);
5278
6227
  const governanceReport = getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query ?? {}));
5279
6228
  return {
5280
6229
  service: SERVICE_NAME,
5281
6230
  ready: true,
5282
6231
  runtimeMode: normalized.Runtime?.mode ?? "local",
5283
6232
  remote,
6233
+ remoteRegistration,
5284
6234
  compiledModels: summarizeCompiledModels(normalized),
5285
6235
  governance: summarizeGovernanceAlerts(governanceReport),
5286
6236
  issueReport: buildValidationIssueReport({
@@ -5510,8 +6460,8 @@ var init_server = __esm({
5510
6460
  reply.send({ success: true, message: "Service restart initiated" });
5511
6461
  setTimeout(() => {
5512
6462
  const { spawn: spawn3 } = require("child_process");
5513
- const { join: join9 } = require("path");
5514
- const cliPath = join9(__dirname, "cli.js");
6463
+ const { join: join10 } = require("path");
6464
+ const cliPath = join10(__dirname, "cli.js");
5515
6465
  const currentPort = config.initialConfig?.PORT;
5516
6466
  const restartArgs = [cliPath, "start", "--daemon"];
5517
6467
  if (currentPort) {
@@ -5534,7 +6484,85 @@ var init_server = __esm({
5534
6484
  });
5535
6485
 
5536
6486
  // src/router/index.ts
5537
- var import_tiktoken, enc, calculateTokenCount, getUseModel, applyModelThinking, router;
6487
+ function evaluateContextFit(compiled, req, tokenCount) {
6488
+ const safeInputTokens = compiled?.capabilities?.safeInputTokens;
6489
+ const contextWindowTokens = compiled?.capabilities?.contextWindowTokens;
6490
+ const outputTokens = getRequestedOutputTokens(req.body);
6491
+ const thinkingTokens = getEffectiveThinkingBudgetTokens(compiled, req.body);
6492
+ const estimatedTotalTokens = tokenCount + outputTokens + thinkingTokens;
6493
+ if (safeInputTokens && tokenCount > safeInputTokens) {
6494
+ return {
6495
+ fits: false,
6496
+ code: "safe_input_exceeded",
6497
+ inputTokens: tokenCount,
6498
+ estimatedTotalTokens,
6499
+ limit: safeInputTokens
6500
+ };
6501
+ }
6502
+ if (contextWindowTokens && estimatedTotalTokens > contextWindowTokens) {
6503
+ return {
6504
+ fits: false,
6505
+ code: "context_window_exceeded",
6506
+ inputTokens: tokenCount,
6507
+ estimatedTotalTokens,
6508
+ limit: contextWindowTokens
6509
+ };
6510
+ }
6511
+ return {
6512
+ fits: true,
6513
+ inputTokens: tokenCount,
6514
+ estimatedTotalTokens
6515
+ };
6516
+ }
6517
+ function applyContextWindowGuard(req, config, selectedModel, tokenCount) {
6518
+ if (!selectedModel) {
6519
+ return selectedModel;
6520
+ }
6521
+ const selectedCompiled = getCompiledModelRef(config, selectedModel);
6522
+ const selectedFit = evaluateContextFit(selectedCompiled, req, tokenCount);
6523
+ if (selectedFit.fits) {
6524
+ return selectedModel;
6525
+ }
6526
+ const longContextModel = config.Router.longContext ? resolveModelReference(config, config.Router.longContext) : void 0;
6527
+ if (longContextModel && longContextModel !== selectedModel) {
6528
+ const longContextCompiled = getCompiledModelRef(config, longContextModel);
6529
+ const longContextFit = evaluateContextFit(longContextCompiled, req, tokenCount);
6530
+ if (longContextFit.fits) {
6531
+ log(
6532
+ "Using long context model due to selected model context capacity:",
6533
+ selectedModel,
6534
+ "->",
6535
+ longContextModel,
6536
+ "input tokens:",
6537
+ selectedFit.inputTokens,
6538
+ "estimated total tokens:",
6539
+ selectedFit.estimatedTotalTokens,
6540
+ "limit:",
6541
+ selectedFit.limit
6542
+ );
6543
+ if (req.governanceTrace) {
6544
+ appendTraceReason(
6545
+ req.governanceTrace,
6546
+ `context_window_fallback:${selectedCompiled?.id ?? selectedModel}->${longContextCompiled?.id ?? longContextModel}`
6547
+ );
6548
+ }
6549
+ return longContextModel;
6550
+ }
6551
+ }
6552
+ req.contextWindowExceeded = {
6553
+ code: selectedFit.code,
6554
+ model: selectedCompiled?.id ?? selectedModel,
6555
+ inputTokens: selectedFit.inputTokens,
6556
+ estimatedTotalTokens: selectedFit.estimatedTotalTokens,
6557
+ limit: selectedFit.limit,
6558
+ longContextModel: config.Router.longContext
6559
+ };
6560
+ if (req.governanceTrace) {
6561
+ appendTraceReason(req.governanceTrace, `context_window_exceeded:${selectedCompiled?.id ?? selectedModel}`);
6562
+ }
6563
+ return selectedModel;
6564
+ }
6565
+ var import_tiktoken, enc, calculateTokenCount, getUseModel, applyModelThinking, readPositiveInteger, getRequestedOutputTokens, getThinkingBudgetTokens, getEffectiveThinkingBudgetTokens, router;
5538
6566
  var init_router = __esm({
5539
6567
  "src/router/index.ts"() {
5540
6568
  "use strict";
@@ -5662,6 +6690,22 @@ var init_router = __esm({
5662
6690
  req.body.thinking.budget_tokens = thinking.budget_tokens;
5663
6691
  }
5664
6692
  };
6693
+ readPositiveInteger = (value) => {
6694
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
6695
+ };
6696
+ getRequestedOutputTokens = (body) => {
6697
+ return readPositiveInteger(body?.max_tokens) ?? readPositiveInteger(body?.max_completion_tokens) ?? 0;
6698
+ };
6699
+ getThinkingBudgetTokens = (body) => {
6700
+ return readPositiveInteger(body?.thinking?.budget_tokens) ?? 0;
6701
+ };
6702
+ getEffectiveThinkingBudgetTokens = (compiled, body) => {
6703
+ const modelThinking = compiled?.thinking;
6704
+ if (modelThinking?.mode === "off") {
6705
+ return 0;
6706
+ }
6707
+ return readPositiveInteger(modelThinking?.budget_tokens) ?? getThinkingBudgetTokens(body);
6708
+ };
5665
6709
  router = async (req, _res, context) => {
5666
6710
  const { config, event: event2 } = context;
5667
6711
  if (req.body.metadata?.user_id) {
@@ -5693,7 +6737,7 @@ var init_router = __esm({
5693
6737
  if (!model && !req.body.model.includes(",")) {
5694
6738
  model = await getUseModel(req, tokenCount, config, lastMessageUsage);
5695
6739
  }
5696
- req.body.model = model ?? req.body.model;
6740
+ req.body.model = applyContextWindowGuard(req, config, model ?? req.body.model, tokenCount);
5697
6741
  applyModelThinking(req, config, req.body.model);
5698
6742
  const compiledModel = getCompiledModelRef(config, req.body.model);
5699
6743
  if (compiledModel?.source === "registration" && compiledModel.modelPool) {
@@ -5733,6 +6777,7 @@ function authRequirementForRequest(req) {
5733
6777
  "/api/remote-status",
5734
6778
  "/api/registration",
5735
6779
  "/api/models/compiled",
6780
+ "/api/models/pool-health",
5736
6781
  "/api/transformers",
5737
6782
  "/api/governance/health",
5738
6783
  "/api/governance/metrics",
@@ -5743,12 +6788,21 @@ function authRequirementForRequest(req) {
5743
6788
  "/v1/messages",
5744
6789
  "/v1/chat/completions"
5745
6790
  ]);
6791
+ const operatorWritePaths = /* @__PURE__ */ new Set([
6792
+ "/api/restart",
6793
+ "/api/governance/metrics/snapshots",
6794
+ "/api/governance/metrics/schedules",
6795
+ "/api/governance/observability/anomaly-thresholds"
6796
+ ]);
5746
6797
  if (method === "GET" && (readOnlyPaths.has(path) || path === "/api/governance/traces" || path.startsWith("/api/governance/traces/") || path === "/api/governance/archives" || path.startsWith("/api/governance/archives/"))) {
5747
6798
  return "read-only";
5748
6799
  }
5749
6800
  if (modelCallPaths.has(path)) {
5750
6801
  return "client";
5751
6802
  }
6803
+ if (method === "POST" && (operatorWritePaths.has(path) || path.startsWith("/api/governance/archives/") && path.endsWith("/delete"))) {
6804
+ return "operator";
6805
+ }
5752
6806
  return path.startsWith("/api/") || path === "/ui" ? "admin" : "client";
5753
6807
  }
5754
6808
  function isQuotaMeteredRequest(req) {
@@ -5852,7 +6906,7 @@ async function loadPersistedAuthQuotaUsage() {
5852
6906
  if (!(0, import_fs4.existsSync)(QUOTA_USAGE_FILE)) {
5853
6907
  return void 0;
5854
6908
  }
5855
- const content = await (0, import_promises2.readFile)(QUOTA_USAGE_FILE, "utf-8");
6909
+ const content = await (0, import_promises3.readFile)(QUOTA_USAGE_FILE, "utf-8");
5856
6910
  const parsed = JSON.parse(content);
5857
6911
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5858
6912
  }
@@ -5862,17 +6916,17 @@ async function savePersistedAuthQuotaUsage(usage) {
5862
6916
  }
5863
6917
  const tempFile = `${QUOTA_USAGE_FILE}.tmp`;
5864
6918
  quotaUsageWriteQueue = quotaUsageWriteQueue.catch(() => void 0).then(async () => {
5865
- await (0, import_promises2.writeFile)(tempFile, JSON.stringify(usage, null, 2), "utf-8");
5866
- await (0, import_promises2.rename)(tempFile, QUOTA_USAGE_FILE);
6919
+ await (0, import_promises3.writeFile)(tempFile, JSON.stringify(usage, null, 2), "utf-8");
6920
+ await (0, import_promises3.rename)(tempFile, QUOTA_USAGE_FILE);
5867
6921
  });
5868
6922
  await quotaUsageWriteQueue;
5869
6923
  }
5870
- var import_fs4, import_promises2, import_path5, QUOTA_USAGE_FILE, quotaUsageWriteQueue;
6924
+ var import_fs4, import_promises3, import_path5, QUOTA_USAGE_FILE, quotaUsageWriteQueue;
5871
6925
  var init_quota_persistence = __esm({
5872
6926
  "src/auth/quota-persistence.ts"() {
5873
6927
  "use strict";
5874
6928
  import_fs4 = require("fs");
5875
- import_promises2 = require("fs/promises");
6929
+ import_promises3 = require("fs/promises");
5876
6930
  import_path5 = require("path");
5877
6931
  init_constants();
5878
6932
  QUOTA_USAGE_FILE = (0, import_path5.join)(HOME_DIR, "auth-quota-usage.json");
@@ -7450,6 +8504,92 @@ var init_trigger = __esm({
7450
8504
  }
7451
8505
  });
7452
8506
 
8507
+ // src/models/pool-health-persistence.ts
8508
+ async function loadPersistedModelPoolHealth() {
8509
+ if (!(0, import_fs6.existsSync)(MODEL_POOL_HEALTH_FILE)) {
8510
+ return void 0;
8511
+ }
8512
+ const content = await (0, import_promises4.readFile)(MODEL_POOL_HEALTH_FILE, "utf-8");
8513
+ const parsed = JSON.parse(content);
8514
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
8515
+ }
8516
+ async function savePersistedModelPoolHealth(payload) {
8517
+ if (!(0, import_fs6.existsSync)(CONFIG_DIR)) {
8518
+ (0, import_fs6.mkdirSync)(CONFIG_DIR, { recursive: true });
8519
+ }
8520
+ const tempFile = `${MODEL_POOL_HEALTH_FILE}.tmp`;
8521
+ modelPoolHealthWriteQueue = modelPoolHealthWriteQueue.catch(() => void 0).then(async () => {
8522
+ await (0, import_promises4.writeFile)(tempFile, JSON.stringify(payload, null, 2), "utf-8");
8523
+ await (0, import_promises4.rename)(tempFile, MODEL_POOL_HEALTH_FILE);
8524
+ });
8525
+ await modelPoolHealthWriteQueue;
8526
+ }
8527
+ function createModelPoolHealthPersistenceScheduler(options = {}) {
8528
+ const debounceMs = options.debounceMs ?? 25;
8529
+ const save = options.save ?? savePersistedModelPoolHealth;
8530
+ const onError = options.onError;
8531
+ let pendingPayload;
8532
+ let persistTimer;
8533
+ let persistQueue = Promise.resolve();
8534
+ const enqueue = (payload) => {
8535
+ persistQueue = persistQueue.catch(() => void 0).then(async () => {
8536
+ try {
8537
+ await save(payload);
8538
+ } catch (error) {
8539
+ onError?.(error);
8540
+ }
8541
+ });
8542
+ return persistQueue;
8543
+ };
8544
+ const flush = async (payload) => {
8545
+ if (payload) {
8546
+ pendingPayload = payload;
8547
+ }
8548
+ if (persistTimer) {
8549
+ clearTimeout(persistTimer);
8550
+ persistTimer = void 0;
8551
+ }
8552
+ const nextPayload = pendingPayload;
8553
+ pendingPayload = void 0;
8554
+ if (nextPayload) {
8555
+ await enqueue(nextPayload);
8556
+ return;
8557
+ }
8558
+ await persistQueue;
8559
+ };
8560
+ const schedule = (payload) => {
8561
+ pendingPayload = payload;
8562
+ if (persistTimer) {
8563
+ return;
8564
+ }
8565
+ persistTimer = setTimeout(() => {
8566
+ persistTimer = void 0;
8567
+ const nextPayload = pendingPayload;
8568
+ pendingPayload = void 0;
8569
+ if (nextPayload) {
8570
+ void enqueue(nextPayload);
8571
+ }
8572
+ }, debounceMs);
8573
+ persistTimer.unref?.();
8574
+ };
8575
+ return {
8576
+ schedule,
8577
+ flush
8578
+ };
8579
+ }
8580
+ var import_fs6, import_promises4, import_path6, MODEL_POOL_HEALTH_FILE, modelPoolHealthWriteQueue;
8581
+ var init_pool_health_persistence = __esm({
8582
+ "src/models/pool-health-persistence.ts"() {
8583
+ "use strict";
8584
+ import_fs6 = require("fs");
8585
+ import_promises4 = require("fs/promises");
8586
+ import_path6 = require("path");
8587
+ init_constants();
8588
+ MODEL_POOL_HEALTH_FILE = (0, import_path6.join)(CONFIG_DIR, "model-pool-health.json");
8589
+ modelPoolHealthWriteQueue = Promise.resolve();
8590
+ }
8591
+ });
8592
+
7453
8593
  // src/protocols/openai.ts
7454
8594
  function toOpenAIContent(parts) {
7455
8595
  const contentParts = parts.filter(
@@ -7800,8 +8940,8 @@ function cloneRequestBody(value) {
7800
8940
  }
7801
8941
  async function initializeClaudeConfig() {
7802
8942
  const homeDir = (0, import_os2.homedir)();
7803
- const configPath = (0, import_path6.join)(homeDir, ".claude.json");
7804
- if (!(0, import_fs6.existsSync)(configPath)) {
8943
+ const configPath = (0, import_path7.join)(homeDir, ".claude.json");
8944
+ if (!(0, import_fs7.existsSync)(configPath)) {
7805
8945
  log(`Creating ${configPath} for Claude Code compatibility (onboarding bypass)`);
7806
8946
  const userID = Array.from(
7807
8947
  { length: 64 },
@@ -7815,22 +8955,106 @@ async function initializeClaudeConfig() {
7815
8955
  lastOnboardingVersion: "1.0.17",
7816
8956
  projects: {}
7817
8957
  };
7818
- await (0, import_promises3.writeFile)(configPath, JSON.stringify(configContent, null, 2));
8958
+ await (0, import_promises5.writeFile)(configPath, JSON.stringify(configContent, null, 2));
8959
+ }
8960
+ }
8961
+ function buildServerInitialConfig(config, registry, host, servicePort) {
8962
+ return {
8963
+ ...config,
8964
+ providers: registry.providers,
8965
+ HOST: host,
8966
+ PORT: servicePort,
8967
+ LOG_FILE: (0, import_path7.join)(
8968
+ (0, import_os2.homedir)(),
8969
+ ".claude-trigger-router",
8970
+ "claude-trigger-router.log"
8971
+ )
8972
+ };
8973
+ }
8974
+ function isRemoteForwardEnabled(config) {
8975
+ const runtime = config?.Runtime ?? {};
8976
+ const remoteService = runtime.remote_service ?? {};
8977
+ return (runtime.mode ?? "local") === "local" && Boolean(remoteService.enabled) && typeof remoteService.base_url === "string" && remoteService.base_url.trim().length > 0;
8978
+ }
8979
+ function isModelCallPath(url) {
8980
+ const path = String(url ?? "").split("?")[0];
8981
+ return path === "/v1/messages" || path === "/v1/chat/completions";
8982
+ }
8983
+ function getRemoteForwardPath(url) {
8984
+ const requestUrl = String(url ?? "");
8985
+ return requestUrl.startsWith("/") ? requestUrl : `/${requestUrl}`;
8986
+ }
8987
+ function buildRemoteForwardHeaders(req, authToken) {
8988
+ const headers = {};
8989
+ const passThroughHeaders = [
8990
+ "accept",
8991
+ "anthropic-version",
8992
+ "anthropic-beta",
8993
+ "content-type"
8994
+ ];
8995
+ for (const header of passThroughHeaders) {
8996
+ const value = req.headers?.[header];
8997
+ if (typeof value === "string" && value.trim()) {
8998
+ headers[header] = value;
8999
+ }
9000
+ }
9001
+ if (!headers["content-type"]) {
9002
+ headers["content-type"] = "application/json";
9003
+ }
9004
+ if (authToken?.trim()) {
9005
+ headers.Authorization = `Bearer ${authToken.trim()}`;
9006
+ }
9007
+ headers["x-ctr-remote-forward"] = "1";
9008
+ return headers;
9009
+ }
9010
+ async function forwardModelCallToRemote(req, reply, config) {
9011
+ if (!isModelCallPath(req.url) || !isRemoteForwardEnabled(config) || req.headers?.["x-ctr-remote-forward"] === "1") {
9012
+ return false;
9013
+ }
9014
+ const remoteService = config.Runtime.remote_service;
9015
+ const remoteBaseUrl = remoteService.base_url.trim().replace(/\/+$/, "");
9016
+ const forwardPath = getRemoteForwardPath(req.url);
9017
+ const path = forwardPath.split("?")[0];
9018
+ const targetUrl = `${remoteBaseUrl}${forwardPath}`;
9019
+ try {
9020
+ const response = await fetch(targetUrl, {
9021
+ method: String(req.method ?? "POST").toUpperCase(),
9022
+ headers: buildRemoteForwardHeaders(req, remoteService.auth_token),
9023
+ body: req.body === void 0 ? void 0 : JSON.stringify(req.body),
9024
+ signal: AbortSignal.timeout(config.API_TIMEOUT_MS ?? 6e5)
9025
+ });
9026
+ reply.code(response.status);
9027
+ const contentType = response.headers.get("content-type");
9028
+ if (contentType) {
9029
+ reply.header?.("content-type", contentType);
9030
+ }
9031
+ const retryAfter = response.headers.get("retry-after");
9032
+ if (retryAfter) {
9033
+ reply.header?.("retry-after", retryAfter);
9034
+ }
9035
+ req.remoteForwarded = true;
9036
+ req.responseGovernanceApplied = true;
9037
+ if (response.body) {
9038
+ reply.send(response.body);
9039
+ return true;
9040
+ }
9041
+ reply.send(response.ok ? {} : { error: `Remote service returned HTTP ${response.status}` });
9042
+ return true;
9043
+ } catch (error) {
9044
+ req.remoteForwarded = true;
9045
+ req.responseGovernanceApplied = true;
9046
+ logWarn(`[RemoteForward] Failed to forward ${path}: ${error instanceof Error ? error.message : String(error)}`);
9047
+ reply.code(502);
9048
+ reply.send({
9049
+ error: {
9050
+ type: "remote_service_unavailable",
9051
+ message: "Remote CTR service is unavailable.",
9052
+ remoteService: remoteBaseUrl
9053
+ }
9054
+ });
9055
+ return true;
7819
9056
  }
7820
9057
  }
7821
- function buildServerInitialConfig(config, registry, host, servicePort) {
7822
- return {
7823
- ...config,
7824
- providers: registry.providers,
7825
- HOST: host,
7826
- PORT: servicePort,
7827
- LOG_FILE: (0, import_path6.join)(
7828
- (0, import_os2.homedir)(),
7829
- ".claude-trigger-router",
7830
- "claude-trigger-router.log"
7831
- )
7832
- };
7833
- }
7834
9058
  async function run(options = {}) {
7835
9059
  if (isServiceRunning()) {
7836
9060
  log("\u2705 Service is already running in the background.");
@@ -7844,6 +9068,17 @@ async function run(options = {}) {
7844
9068
  } catch (error) {
7845
9069
  logWarn(`[AuthQuota] Failed to load persisted quota usage: ${error instanceof Error ? error.message : String(error)}`);
7846
9070
  }
9071
+ try {
9072
+ modelPoolHealthStore.hydrate(await loadPersistedModelPoolHealth());
9073
+ } catch (error) {
9074
+ logWarn(`[ModelPoolHealth] Failed to load persisted health state: ${error instanceof Error ? error.message : String(error)}`);
9075
+ }
9076
+ const modelPoolHealthPersistence = createModelPoolHealthPersistenceScheduler({
9077
+ onError: (error) => {
9078
+ logWarn(`[ModelPoolHealth] Failed to persist health state: ${error instanceof Error ? error.message : String(error)}`);
9079
+ }
9080
+ });
9081
+ modelPoolHealthStore.setChangeListener(modelPoolHealthPersistence.schedule);
7847
9082
  configureLogging(config);
7848
9083
  let HOST = config.HOST || "127.0.0.1";
7849
9084
  const managedKeySummary = managedApiKeySummary(config);
@@ -7854,15 +9089,21 @@ async function run(options = {}) {
7854
9089
  }
7855
9090
  const port = options.port ?? config.PORT ?? DEFAULT_CONFIG.PORT;
7856
9091
  savePid(process.pid, port);
7857
- process.on("SIGINT", () => {
7858
- log("Received SIGINT, cleaning up...");
7859
- cleanupPidFile();
7860
- process.exit(0);
7861
- });
7862
- process.on("SIGTERM", () => {
9092
+ const shutdown = (signal) => {
9093
+ log(`Received ${signal}, cleaning up...`);
7863
9094
  cleanupPidFile();
7864
- process.exit(0);
7865
- });
9095
+ const forceExit = setTimeout(() => process.exit(0), 500);
9096
+ forceExit.unref?.();
9097
+ void Promise.allSettled([
9098
+ governanceTraceStore.flushPersistence(),
9099
+ modelPoolHealthPersistence.flush(modelPoolHealthStore.exportForPersistence())
9100
+ ]).finally(() => {
9101
+ clearTimeout(forceExit);
9102
+ process.exit(0);
9103
+ });
9104
+ };
9105
+ process.on("SIGINT", () => shutdown("SIGINT"));
9106
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
7866
9107
  const servicePort = process.env.SERVICE_PORT ? parseInt(process.env.SERVICE_PORT) : port;
7867
9108
  config.PORT = servicePort;
7868
9109
  const pad = (num) => (num > 9 ? "" : "0") + num;
@@ -7893,19 +9134,37 @@ async function run(options = {}) {
7893
9134
  initialConfig: buildServerInitialConfig(config, registry, HOST, servicePort),
7894
9135
  logger: loggerConfig
7895
9136
  });
7896
- const authMiddleware = apiKeyAuth(async () => {
7897
- try {
7898
- const currentConfig = await readConfigFile();
7899
- return {
9137
+ const authConfigTtlMs = 1e3;
9138
+ let cachedAuthConfig;
9139
+ let cachedAuthConfigExpiresAt = 0;
9140
+ let pendingAuthConfigRefresh;
9141
+ const getAuthConfig = async () => {
9142
+ const now = Date.now();
9143
+ if (cachedAuthConfig && now < cachedAuthConfigExpiresAt) {
9144
+ return cachedAuthConfig;
9145
+ }
9146
+ if (pendingAuthConfigRefresh) {
9147
+ return pendingAuthConfigRefresh;
9148
+ }
9149
+ pendingAuthConfigRefresh = readConfigFile().then((currentConfig) => {
9150
+ cachedAuthConfig = {
7900
9151
  ...config,
7901
9152
  APIKEY: currentConfig.APIKEY,
7902
9153
  Auth: currentConfig.Auth
7903
9154
  };
7904
- } catch (error) {
9155
+ cachedAuthConfigExpiresAt = Date.now() + authConfigTtlMs;
9156
+ return cachedAuthConfig;
9157
+ }).catch((error) => {
7905
9158
  logWarn(`[Auth] Failed to refresh auth config, using startup auth config: ${error instanceof Error ? error.message : String(error)}`);
9159
+ cachedAuthConfig = config;
9160
+ cachedAuthConfigExpiresAt = Date.now() + authConfigTtlMs;
7906
9161
  return config;
7907
- }
7908
- }, {
9162
+ }).finally(() => {
9163
+ pendingAuthConfigRefresh = void 0;
9164
+ });
9165
+ return pendingAuthConfigRefresh;
9166
+ };
9167
+ const authMiddleware = apiKeyAuth(getAuthConfig, {
7909
9168
  persistQuotaUsage: async (usage) => {
7910
9169
  try {
7911
9170
  await savePersistedAuthQuotaUsage(usage);
@@ -7923,9 +9182,17 @@ async function run(options = {}) {
7923
9182
  authMiddleware(req, reply, done);
7924
9183
  });
7925
9184
  });
9185
+ server.addHook("preHandler", async (req, reply) => {
9186
+ if (await forwardModelCallToRemote(req, reply, config)) {
9187
+ return reply;
9188
+ }
9189
+ });
7926
9190
  triggerRouter.init(config);
7927
9191
  log(`[SmartRouter] Initialized, enabled: ${triggerRouter.isEnabled()}`);
7928
9192
  server.addHook("preHandler", async (req, reply) => {
9193
+ if (req.remoteForwarded) {
9194
+ return;
9195
+ }
7929
9196
  if (req.url.startsWith("/v1/messages")) {
7930
9197
  if (req.body.metadata?.user_id) {
7931
9198
  const parts = req.body.metadata.user_id.split("_session_");
@@ -8004,6 +9271,24 @@ async function run(options = {}) {
8004
9271
  config,
8005
9272
  event
8006
9273
  });
9274
+ if (req.contextWindowExceeded) {
9275
+ req.responseGovernanceApplied = true;
9276
+ req.localStructuredError = true;
9277
+ if (req.governanceTrace) {
9278
+ req.governanceTrace = finalizeTrace(req.governanceTrace, {
9279
+ finalModel: req.body?.model ?? req.governanceTrace.finalModel
9280
+ });
9281
+ recordGovernanceTrace(req.governanceTrace);
9282
+ }
9283
+ reply.code(413);
9284
+ return reply.send({
9285
+ error: {
9286
+ type: "context_window_exceeded",
9287
+ message: "Selected model cannot safely handle the current request context.",
9288
+ details: req.contextWindowExceeded
9289
+ }
9290
+ });
9291
+ }
8007
9292
  const compiledModel = getCompiledModelRef(config, req.body?.model);
8008
9293
  if (compiledModel?.interface && req.body?.messages) {
8009
9294
  const originalBody = cloneRequestBody(req.body);
@@ -8035,6 +9320,9 @@ async function run(options = {}) {
8035
9320
  event.emit("onError", request, reply, error);
8036
9321
  });
8037
9322
  server.addHook("onSend", (req, reply, payload, done) => {
9323
+ if (req.remoteForwarded) {
9324
+ return done(null, payload);
9325
+ }
8038
9326
  if (req.originalRequestBody) {
8039
9327
  req.body = req.originalRequestBody;
8040
9328
  }
@@ -8176,6 +9464,27 @@ async function run(options = {}) {
8176
9464
  sessionUsageCache.put(req.sessionId, payload.usage);
8177
9465
  }
8178
9466
  if (typeof payload === "object" && payload.error) {
9467
+ if (req.localStructuredError || payload.error?.type === "context_window_exceeded") {
9468
+ return done(null, payload);
9469
+ }
9470
+ if (req.modelPoolSelection) {
9471
+ applyResponseGovernance({
9472
+ req,
9473
+ payload,
9474
+ config,
9475
+ servicePort
9476
+ }).then((governedPayload) => {
9477
+ req.responseGovernanceApplied = true;
9478
+ if (governedPayload && typeof governedPayload === "object" && governedPayload.error) {
9479
+ return done(governedPayload.error, null);
9480
+ }
9481
+ if (req.sessionId && governedPayload?.usage) {
9482
+ sessionUsageCache.put(req.sessionId, governedPayload.usage);
9483
+ }
9484
+ return done(null, governedPayload);
9485
+ }).catch((error) => done(error, null));
9486
+ return;
9487
+ }
8179
9488
  return done(payload.error, null);
8180
9489
  }
8181
9490
  done(null, payload);
@@ -8184,12 +9493,14 @@ async function run(options = {}) {
8184
9493
  if (payload instanceof ReadableStream) {
8185
9494
  return payload;
8186
9495
  }
8187
- payload = await applyResponseGovernance({
8188
- req,
8189
- payload,
8190
- config,
8191
- servicePort
8192
- });
9496
+ if (!req.responseGovernanceApplied) {
9497
+ payload = await applyResponseGovernance({
9498
+ req,
9499
+ payload,
9500
+ config,
9501
+ servicePort
9502
+ });
9503
+ }
8193
9504
  if (req.governanceTrace) {
8194
9505
  logDebug("[GovernanceTrace]", JSON.stringify(req.governanceTrace));
8195
9506
  }
@@ -8199,14 +9510,14 @@ async function run(options = {}) {
8199
9510
  });
8200
9511
  await server.start();
8201
9512
  }
8202
- var import_fs6, import_promises3, import_os2, import_path6, import_json5, import_node_events, import_rotating_file_stream, event;
9513
+ var import_fs7, import_promises5, import_os2, import_path7, import_json5, import_node_events, import_rotating_file_stream, event;
8203
9514
  var init_index = __esm({
8204
9515
  "src/index.ts"() {
8205
9516
  "use strict";
8206
- import_fs6 = require("fs");
8207
- import_promises3 = require("fs/promises");
9517
+ import_fs7 = require("fs");
9518
+ import_promises5 = require("fs/promises");
8208
9519
  import_os2 = require("os");
8209
- import_path6 = require("path");
9520
+ import_path7 = require("path");
8210
9521
  init_utils();
8211
9522
  init_server();
8212
9523
  init_router();
@@ -8227,6 +9538,8 @@ var init_index = __esm({
8227
9538
  import_rotating_file_stream = require("rotating-file-stream");
8228
9539
  init_governance();
8229
9540
  init_compile();
9541
+ init_pool_health();
9542
+ init_pool_health_persistence();
8230
9543
  init_protocols();
8231
9544
  event = new import_node_events.EventEmitter();
8232
9545
  }
@@ -9053,7 +10366,7 @@ var init_setup = __esm({
9053
10366
  // src/setup/index.ts
9054
10367
  function createConsoleIO() {
9055
10368
  if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
9056
- const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
10369
+ const scriptedInput = (0, import_fs8.readFileSync)(0, "utf-8");
9057
10370
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
9058
10371
  let cursor = 0;
9059
10372
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -9142,7 +10455,7 @@ function createConsoleIO() {
9142
10455
  }
9143
10456
  };
9144
10457
  }
9145
- const rl = (0, import_promises4.createInterface)({ input: import_process.stdin, output: import_process.stdout });
10458
+ const rl = (0, import_promises6.createInterface)({ input: import_process.stdin, output: import_process.stdout });
9146
10459
  const ask = async (message) => {
9147
10460
  const answer = await rl.question(message);
9148
10461
  return answer.trim();
@@ -9183,7 +10496,7 @@ function createConsoleIO() {
9183
10496
  };
9184
10497
  }
9185
10498
  function readStructuredConfigFile(filePath) {
9186
- const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
10499
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
9187
10500
  if (filePath.endsWith(".json")) {
9188
10501
  return JSON.parse(content);
9189
10502
  }
@@ -9191,7 +10504,7 @@ function readStructuredConfigFile(filePath) {
9191
10504
  }
9192
10505
  function getCurrentRuntimeFields() {
9193
10506
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
9194
- const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
10507
+ const currentPath = candidates.find((filePath) => (0, import_fs8.existsSync)(filePath));
9195
10508
  if (!currentPath) {
9196
10509
  return {};
9197
10510
  }
@@ -9213,7 +10526,7 @@ function getCurrentRuntimeFields() {
9213
10526
  }
9214
10527
  function getConfiguredPortFromCurrentFiles() {
9215
10528
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
9216
- const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
10529
+ const currentPath = candidates.find((filePath) => (0, import_fs8.existsSync)(filePath));
9217
10530
  if (!currentPath) {
9218
10531
  return DEFAULT_CONFIG2.PORT;
9219
10532
  }
@@ -9247,7 +10560,7 @@ async function getAvailablePort() {
9247
10560
  }
9248
10561
  }
9249
10562
  function readLegacyConfigFile(filePath) {
9250
- const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
10563
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
9251
10564
  if (filePath.endsWith(".json")) {
9252
10565
  return import_json52.default.parse(content);
9253
10566
  }
@@ -9255,13 +10568,13 @@ function readLegacyConfigFile(filePath) {
9255
10568
  }
9256
10569
  async function readLegacyConfig(deps = {}) {
9257
10570
  const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
9258
- const exists = deps.exists || import_fs7.existsSync;
10571
+ const exists = deps.exists || import_fs8.existsSync;
9259
10572
  const readConfig = deps.readConfig || readLegacyConfigFile;
9260
10573
  const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
9261
10574
  const candidatePaths = overridePath ? [overridePath] : [
9262
- (0, import_path7.join)(baseHomeDir, ".ccr", "config.yaml"),
9263
- (0, import_path7.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
9264
- (0, import_path7.join)(baseHomeDir, ".claude-code-router", "config.json")
10575
+ (0, import_path8.join)(baseHomeDir, ".ccr", "config.yaml"),
10576
+ (0, import_path8.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
10577
+ (0, import_path8.join)(baseHomeDir, ".claude-code-router", "config.json")
9265
10578
  ];
9266
10579
  const legacyPath = candidatePaths.find((filePath) => exists(filePath));
9267
10580
  if (!legacyPath) {
@@ -9283,7 +10596,7 @@ async function readLegacyConfig(deps = {}) {
9283
10596
  }
9284
10597
  async function readCurrentConfig() {
9285
10598
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
9286
- const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
10599
+ const currentPath = candidates.find((filePath) => (0, import_fs8.existsSync)(filePath));
9287
10600
  if (!currentPath) {
9288
10601
  return { kind: "missing" };
9289
10602
  }
@@ -9967,16 +11280,16 @@ async function runSetupCli(customDeps) {
9967
11280
  deps.io.close?.();
9968
11281
  }
9969
11282
  }
9970
- var import_fs7, import_crypto3, import_net2, import_os3, import_path7, import_promises4, import_process, import_json52, import_js_yaml;
11283
+ var import_fs8, import_crypto3, import_net2, import_os3, import_path8, import_promises6, import_process, import_json52, import_js_yaml;
9971
11284
  var init_setup2 = __esm({
9972
11285
  "src/setup/index.ts"() {
9973
11286
  "use strict";
9974
- import_fs7 = require("fs");
11287
+ import_fs8 = require("fs");
9975
11288
  import_crypto3 = require("crypto");
9976
11289
  import_net2 = require("net");
9977
11290
  import_os3 = require("os");
9978
- import_path7 = require("path");
9979
- import_promises4 = require("readline/promises");
11291
+ import_path8 = require("path");
11292
+ import_promises6 = require("readline/promises");
9980
11293
  import_process = require("process");
9981
11294
  import_json52 = __toESM(require("json5"));
9982
11295
  import_js_yaml = __toESM(require("js-yaml"));
@@ -10067,7 +11380,7 @@ function hasArg(flag) {
10067
11380
  }
10068
11381
  function createConsoleIO2() {
10069
11382
  if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
10070
- const scriptedInput = (0, import_fs8.readFileSync)(0, "utf-8");
11383
+ const scriptedInput = (0, import_fs9.readFileSync)(0, "utf-8");
10071
11384
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
10072
11385
  let cursor = 0;
10073
11386
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -10110,7 +11423,7 @@ function createConsoleIO2() {
10110
11423
  }
10111
11424
  };
10112
11425
  }
10113
- const rl = (0, import_promises5.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
11426
+ const rl = (0, import_promises7.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
10114
11427
  const ask = async (message) => {
10115
11428
  try {
10116
11429
  return (await rl.question(message)).trim();
@@ -10203,7 +11516,7 @@ function tryLoadStructuredConfig(filePath, content) {
10203
11516
  }
10204
11517
  }
10205
11518
  function loadCurrentConfig() {
10206
- const existingPath = getConfigCandidates().find((filePath) => (0, import_fs8.existsSync)(filePath));
11519
+ const existingPath = getConfigCandidates().find((filePath) => (0, import_fs9.existsSync)(filePath));
10207
11520
  const path = existingPath ?? CONFIG_FILE;
10208
11521
  if (!existingPath) {
10209
11522
  return {
@@ -10213,7 +11526,7 @@ function loadCurrentConfig() {
10213
11526
  messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
10214
11527
  };
10215
11528
  }
10216
- const content = (0, import_fs8.readFileSync)(existingPath, "utf-8");
11529
+ const content = (0, import_fs9.readFileSync)(existingPath, "utf-8");
10217
11530
  const loaded = tryLoadStructuredConfig(existingPath, content);
10218
11531
  return {
10219
11532
  path,
@@ -10532,14 +11845,14 @@ async function reportRuntimeServiceContext(config, deps) {
10532
11845
  deps.io.info(`\u670D\u52A1\u4E0A\u4E0B\u6587\uFF1A${runtimeMode}\uFF08${serviceRole}\uFF09`);
10533
11846
  deps.io.info(`\u76D1\u542C\u5730\u5740\uFF1A${host}:${port}${publicHost ? "\uFF08\u5BF9\u5916\u76D1\u542C\uFF09" : "\uFF08\u672C\u673A\u76D1\u542C\uFF09"}`);
10534
11847
  deps.io.info(`\u9274\u6743\u72B6\u6001\uFF1A${authRequired ? "enabled" : "disabled"}\uFF08bootstrap=${hasBootstrapAuth}, managed_active=${managedKeys.active}\uFF09`);
10535
- deps.io.info("Scope \u6307\u5F15\uFF1Aadmin \u7528\u4E8E /ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001\u91CD\u542F\u3001auth \u7BA1\u7406\u548C\u6CBB\u7406\u5199\u64CD\u4F5C\uFF1Bclient \u53EA\u7528\u4E8E\u6A21\u578B\u8C03\u7528\uFF1Bread-only \u53EA\u7528\u4E8E health/status/compiled/governance \u89C2\u6D4B\u3002");
11848
+ deps.io.info("Scope \u6307\u5F15\uFF1Aadmin \u7528\u4E8E /ui\u3001\u914D\u7F6E\u4FDD\u5B58\u548C auth \u7BA1\u7406\uFF1Boperator \u7528\u4E8E\u91CD\u542F\u3001\u6CBB\u7406\u5FEB\u7167\u3001\u5B9A\u65F6\u5FEB\u7167\u3001\u5F02\u5E38\u9608\u503C\u548C\u5F52\u6863\u5220\u9664\uFF1Bclient \u53EA\u7528\u4E8E\u6A21\u578B\u8C03\u7528\uFF1Bread-only \u53EA\u7528\u4E8E health/status/compiled/governance \u89C2\u6D4B\u3002");
10536
11849
  deps.io.info("Key \u64CD\u4F5C\u6307\u5F15\uFF1A\u4F7F\u7528 admin key \u8C03\u7528 GET /api/auth/keys \u67E5\u770B\u5217\u8868\u3001POST /api/auth/keys \u751F\u6210 key\u3001POST /api/auth/keys/:id/revoke \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\u3002");
10537
11850
  if (runtimeMode !== "local") {
10538
11851
  deps.io.info(`\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\uFF1AANTHROPIC_BASE_URL=${listenerUrl}\uFF0CANTHROPIC_AUTH_TOKEN \u4F7F\u7528 managed client + read-only key\u3002`);
10539
11852
  deps.io.info(`\u7EF4\u62A4\u5165\u53E3\uFF1Ahttp://127.0.0.1:${port}/ui\uFF1B\u516C\u7F51\u8BBF\u95EE\u8BF7\u653E\u5728 HTTPS \u53CD\u5411\u4EE3\u7406\u6216\u5185\u7F51\u4E4B\u540E\u3002`);
10540
11853
  }
10541
11854
  if (!authRequired && (runtimeMode !== "local" || publicHost)) {
10542
- deps.io.error("\u5B89\u5168\u98CE\u9669\uFF1A\u5F53\u524D server/cloud \u6216\u516C\u7F51\u76D1\u542C\u672A\u914D\u7F6E API key\uFF1B\u66B4\u9732\u670D\u52A1\u524D\u8BF7\u8BBE\u7F6E APIKEY \u6216\u521B\u5EFA managed client/admin key\u3002");
11855
+ deps.io.error("\u5B89\u5168\u98CE\u9669\uFF1A\u5F53\u524D server/cloud \u6216\u516C\u7F51\u76D1\u542C\u672A\u914D\u7F6E API key\uFF1B\u66B4\u9732\u670D\u52A1\u524D\u8BF7\u8BBE\u7F6E APIKEY \u6216\u521B\u5EFA managed client/operator/admin key\u3002");
10543
11856
  } else if (!hasBootstrapAuth && hasManagedAuthRecords && managedKeys.active === 0) {
10544
11857
  deps.io.error("\u5B89\u5168\u98CE\u9669\uFF1A\u5F53\u524D\u4EC5\u4FDD\u7559 managed key \u8BB0\u5F55\u4F46\u6CA1\u6709 active key\uFF1B\u670D\u52A1\u4F1A\u62D2\u7EDD\u8BF7\u6C42\uFF0C\u8BF7\u8BBE\u7F6E APIKEY \u6216\u521B\u5EFA active managed key\u3002");
10545
11858
  } else if (authRequired && hasBootstrapAuth && managedKeys.total === 0 && runtimeMode !== "local") {
@@ -10691,12 +12004,12 @@ async function runDoctorCli(customDeps) {
10691
12004
  deps.io.close?.();
10692
12005
  }
10693
12006
  }
10694
- var import_fs8, import_promises5, import_process2, import_child_process2, import_json53, import_js_yaml2;
12007
+ var import_fs9, import_promises7, import_process2, import_child_process2, import_json53, import_js_yaml2;
10695
12008
  var init_doctor = __esm({
10696
12009
  "src/doctor/index.ts"() {
10697
12010
  "use strict";
10698
- import_fs8 = require("fs");
10699
- import_promises5 = require("readline/promises");
12011
+ import_fs9 = require("fs");
12012
+ import_promises7 = require("readline/promises");
10700
12013
  import_process2 = require("process");
10701
12014
  import_child_process2 = require("child_process");
10702
12015
  import_json53 = __toESM(require("json5"));
@@ -10716,6 +12029,583 @@ var init_doctor = __esm({
10716
12029
  }
10717
12030
  });
10718
12031
 
12032
+ // src/governance/task-evaluation.ts
12033
+ function parseOfflineEvaluationInputs(payload) {
12034
+ const rawResults = Array.isArray(payload) ? payload : typeof payload === "object" && payload !== null && Array.isArray(payload.results) ? payload.results : void 0;
12035
+ if (!rawResults) {
12036
+ throw new Error("\u8BC4\u6D4B\u8F93\u5165\u5FC5\u987B\u662F\u6570\u7EC4\uFF0C\u6216\u5305\u542B results \u6570\u7EC4\u5B57\u6BB5\u7684\u5BF9\u8C61\u3002");
12037
+ }
12038
+ return rawResults.map((item, index) => {
12039
+ if (typeof item !== "object" || item === null) {
12040
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
12041
+ }
12042
+ const record = item;
12043
+ if (typeof record.taskId !== "string" || !record.taskId.trim()) {
12044
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7F3A\u5C11 taskId\u3002`);
12045
+ }
12046
+ if (typeof record.model !== "string" || !record.model.trim()) {
12047
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7F3A\u5C11 model\u3002`);
12048
+ }
12049
+ if (record.output !== void 0 && typeof record.output !== "string") {
12050
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 output \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
12051
+ }
12052
+ if (record.error !== void 0 && typeof record.error !== "string") {
12053
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 error \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
12054
+ }
12055
+ if (record.latencyMs !== void 0 && (typeof record.latencyMs !== "number" || !Number.isFinite(record.latencyMs) || record.latencyMs < 0)) {
12056
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 latencyMs \u5FC5\u987B\u662F\u975E\u8D1F\u6570\u5B57\u3002`);
12057
+ }
12058
+ return {
12059
+ taskId: record.taskId.trim(),
12060
+ model: record.model.trim(),
12061
+ output: record.output,
12062
+ error: record.error,
12063
+ latencyMs: record.latencyMs
12064
+ };
12065
+ });
12066
+ }
12067
+ function clamp(value) {
12068
+ return Math.max(0, Math.min(1, Number(value.toFixed(4))));
12069
+ }
12070
+ function average2(values) {
12071
+ if (!values.length) {
12072
+ return 0;
12073
+ }
12074
+ return Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(4));
12075
+ }
12076
+ function rate2(count, total) {
12077
+ if (!total) {
12078
+ return 0;
12079
+ }
12080
+ return Number((count / total).toFixed(4));
12081
+ }
12082
+ function normalizeText(value) {
12083
+ return value.toLowerCase();
12084
+ }
12085
+ function includesCodeBlock(output3) {
12086
+ return /```[\s\S]*```/.test(output3);
12087
+ }
12088
+ function normalizeDimensionId(value) {
12089
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "quality";
12090
+ }
12091
+ function qualityDimensionsForTask(task) {
12092
+ if (Array.isArray(task.qualityDimensions) && task.qualityDimensions.length) {
12093
+ return task.qualityDimensions.map((dimension) => ({
12094
+ ...dimension,
12095
+ id: normalizeDimensionId(dimension.id)
12096
+ }));
12097
+ }
12098
+ const dimensions = [];
12099
+ if ((task.requiredKeywords ?? []).length) {
12100
+ dimensions.push({
12101
+ id: "semantic_coverage",
12102
+ label: "Semantic coverage",
12103
+ weight: 0.45,
12104
+ minScore: 0.7,
12105
+ requiredKeywords: task.requiredKeywords
12106
+ });
12107
+ }
12108
+ if ((task.minOutputChars ?? 0) > 0) {
12109
+ dimensions.push({
12110
+ id: "completeness",
12111
+ label: "Completeness",
12112
+ weight: 0.25,
12113
+ minScore: 0.7,
12114
+ minOutputChars: task.minOutputChars
12115
+ });
12116
+ }
12117
+ if (task.requiresCodeBlock) {
12118
+ dimensions.push({
12119
+ id: "deliverable_format",
12120
+ label: "Deliverable format",
12121
+ weight: 0.2,
12122
+ minScore: 1,
12123
+ requiresCodeBlock: true
12124
+ });
12125
+ }
12126
+ if ((task.forbiddenPatterns ?? []).length) {
12127
+ dimensions.push({
12128
+ id: "safety_hygiene",
12129
+ label: "Safety and hygiene",
12130
+ weight: 0.1,
12131
+ minScore: 1,
12132
+ forbiddenPatterns: task.forbiddenPatterns
12133
+ });
12134
+ }
12135
+ return dimensions;
12136
+ }
12137
+ function extractResponseText(payload) {
12138
+ if (!payload) {
12139
+ return "";
12140
+ }
12141
+ if (typeof payload === "string") {
12142
+ return payload;
12143
+ }
12144
+ if (typeof payload.output_text === "string") {
12145
+ return payload.output_text;
12146
+ }
12147
+ if (typeof payload.content === "string") {
12148
+ return payload.content;
12149
+ }
12150
+ if (Array.isArray(payload.content)) {
12151
+ return extractContentText(payload.content);
12152
+ }
12153
+ if (Array.isArray(payload.choices)) {
12154
+ return payload.choices.map((choice) => extractContentText(choice?.message?.content ?? choice?.text ?? "")).filter(Boolean).join("\n");
12155
+ }
12156
+ return "";
12157
+ }
12158
+ function extractContentText(content) {
12159
+ if (typeof content === "string") {
12160
+ return content;
12161
+ }
12162
+ if (!Array.isArray(content)) {
12163
+ return "";
12164
+ }
12165
+ return content.map((part) => {
12166
+ if (typeof part === "string") {
12167
+ return part;
12168
+ }
12169
+ if (typeof part?.text === "string") {
12170
+ return part.text;
12171
+ }
12172
+ return "";
12173
+ }).filter(Boolean).join("\n");
12174
+ }
12175
+ function evaluateDimension(dimension, output3, normalized) {
12176
+ const findings = [];
12177
+ const weight = Number.isFinite(dimension.weight) && (dimension.weight ?? 0) > 0 ? dimension.weight : 1;
12178
+ let score = 1;
12179
+ if ((dimension.minOutputChars ?? 0) > 0 && output3.trim().length < (dimension.minOutputChars ?? 0)) {
12180
+ findings.push(`output_too_short:${output3.trim().length}/${dimension.minOutputChars}`);
12181
+ score -= 0.25;
12182
+ }
12183
+ const requiredKeywords = dimension.requiredKeywords ?? [];
12184
+ const missingKeywords = requiredKeywords.filter((keyword) => !normalized.includes(keyword.toLowerCase()));
12185
+ if (missingKeywords.length) {
12186
+ findings.push(`missing_keywords:${missingKeywords.join("|")}`);
12187
+ score -= 0.6 * rate2(missingKeywords.length, Math.max(requiredKeywords.length, 1));
12188
+ }
12189
+ if (dimension.requiresCodeBlock && !includesCodeBlock(output3)) {
12190
+ findings.push("missing_code_block");
12191
+ score -= 0.25;
12192
+ }
12193
+ const forbiddenMatches = (dimension.forbiddenPatterns ?? []).filter((pattern) => normalized.includes(pattern.toLowerCase()));
12194
+ if (forbiddenMatches.length) {
12195
+ findings.push(`forbidden_patterns:${forbiddenMatches.join("|")}`);
12196
+ score -= 0.5;
12197
+ }
12198
+ return {
12199
+ id: normalizeDimensionId(dimension.id),
12200
+ label: dimension.label ?? dimension.id,
12201
+ score: clamp(score),
12202
+ weight,
12203
+ findings
12204
+ };
12205
+ }
12206
+ function weightedAverageDimensionScore(scores) {
12207
+ const totalWeight = scores.reduce((sum, score) => sum + score.weight, 0);
12208
+ if (!scores.length || totalWeight <= 0) {
12209
+ return 1;
12210
+ }
12211
+ return clamp(scores.reduce((sum, score) => sum + score.score * score.weight, 0) / totalWeight);
12212
+ }
12213
+ function averageDimensionScores(runs) {
12214
+ const grouped = {};
12215
+ for (const run2 of runs) {
12216
+ for (const dimension of run2.dimensionScores) {
12217
+ grouped[dimension.id] ??= [];
12218
+ grouped[dimension.id].push(dimension.score);
12219
+ }
12220
+ }
12221
+ return Object.fromEntries(
12222
+ Object.entries(grouped).map(([id, values]) => [id, average2(values)]).sort(([left], [right]) => left.localeCompare(right))
12223
+ );
12224
+ }
12225
+ function evaluateRun(task, input3) {
12226
+ const findings = [];
12227
+ const output3 = input3.output ?? "";
12228
+ const normalized = normalizeText(output3);
12229
+ const minQualityScore = task.minQualityScore ?? 0.7;
12230
+ let qualityScore = 1;
12231
+ let dimensionsPassed = true;
12232
+ if (input3.error) {
12233
+ findings.push(`runner_error:${input3.error}`);
12234
+ qualityScore = 0;
12235
+ }
12236
+ const dimensions = qualityDimensionsForTask(task);
12237
+ const dimensionScores = dimensions.map((dimension) => evaluateDimension(dimension, output3, normalized));
12238
+ for (const dimension of dimensionScores) {
12239
+ const sourceDimension = dimensions.find((item) => normalizeDimensionId(item.id) === dimension.id);
12240
+ const minScore = sourceDimension?.minScore ?? 0.7;
12241
+ if (dimension.score < minScore) {
12242
+ dimensionsPassed = false;
12243
+ findings.push(`dimension_below_threshold:${dimension.id}:${dimension.score}/${minScore}`);
12244
+ }
12245
+ for (const finding of dimension.findings) {
12246
+ findings.push(`dimension_${dimension.id}:${finding}`);
12247
+ }
12248
+ }
12249
+ if ((task.minOutputChars ?? 0) > 0 && output3.trim().length < (task.minOutputChars ?? 0)) {
12250
+ findings.push(`output_too_short:${output3.trim().length}/${task.minOutputChars}`);
12251
+ qualityScore -= 0.25;
12252
+ }
12253
+ const requiredKeywords = task.requiredKeywords ?? [];
12254
+ const missingKeywords = requiredKeywords.filter((keyword) => !normalized.includes(keyword.toLowerCase()));
12255
+ if (missingKeywords.length) {
12256
+ findings.push(`missing_keywords:${missingKeywords.join("|")}`);
12257
+ qualityScore -= 0.35 * rate2(missingKeywords.length, Math.max(requiredKeywords.length, 1));
12258
+ }
12259
+ if (task.requiresCodeBlock && !includesCodeBlock(output3)) {
12260
+ findings.push("missing_code_block");
12261
+ qualityScore -= 0.25;
12262
+ }
12263
+ const forbiddenMatches = (task.forbiddenPatterns ?? []).filter((pattern) => normalized.includes(pattern.toLowerCase()));
12264
+ if (forbiddenMatches.length) {
12265
+ findings.push(`forbidden_patterns:${forbiddenMatches.join("|")}`);
12266
+ qualityScore -= 0.4;
12267
+ }
12268
+ const latencyMs = typeof input3.latencyMs === "number" ? input3.latencyMs : void 0;
12269
+ const speedScore = latencyMs === void 0 || !task.maxLatencyMs ? 0 : clamp(1 - Math.max(0, latencyMs - task.maxLatencyMs) / task.maxLatencyMs);
12270
+ if (latencyMs !== void 0 && task.maxLatencyMs && latencyMs > task.maxLatencyMs) {
12271
+ findings.push(`latency_over_budget:${latencyMs}/${task.maxLatencyMs}`);
12272
+ }
12273
+ const finalQualityScore = clamp(Math.min(qualityScore, weightedAverageDimensionScore(dimensionScores)));
12274
+ return {
12275
+ taskId: task.id,
12276
+ intent: task.intent,
12277
+ model: input3.model,
12278
+ passed: !input3.error && dimensionsPassed && finalQualityScore >= minQualityScore,
12279
+ qualityScore: finalQualityScore,
12280
+ speedScore,
12281
+ latencyMs,
12282
+ dimensionScores,
12283
+ findings
12284
+ };
12285
+ }
12286
+ function summarizeGroup(key, runs) {
12287
+ const latencies = runs.map((run2) => run2.latencyMs).filter((value) => typeof value === "number");
12288
+ return {
12289
+ key,
12290
+ totalRuns: runs.length,
12291
+ passedRuns: runs.filter((run2) => run2.passed).length,
12292
+ passRate: rate2(runs.filter((run2) => run2.passed).length, runs.length),
12293
+ averageQualityScore: average2(runs.map((run2) => run2.qualityScore)),
12294
+ averageSpeedScore: average2(runs.map((run2) => run2.speedScore)),
12295
+ averageLatencyMs: latencies.length ? Number(average2(latencies).toFixed(2)) : 0,
12296
+ averageDimensionScores: averageDimensionScores(runs)
12297
+ };
12298
+ }
12299
+ function groupRuns(runs, keyFn) {
12300
+ const groups = {};
12301
+ for (const run2 of runs) {
12302
+ const key = keyFn(run2);
12303
+ groups[key] ??= [];
12304
+ groups[key].push(run2);
12305
+ }
12306
+ return Object.entries(groups).map(([key, groupRunsForKey]) => summarizeGroup(key, groupRunsForKey)).sort((left, right) => {
12307
+ if (right.passRate !== left.passRate) {
12308
+ return right.passRate - left.passRate;
12309
+ }
12310
+ if (right.averageQualityScore !== left.averageQualityScore) {
12311
+ return right.averageQualityScore - left.averageQualityScore;
12312
+ }
12313
+ return left.averageLatencyMs - right.averageLatencyMs;
12314
+ });
12315
+ }
12316
+ function bestRunForTask(taskId, runs) {
12317
+ return runs.filter((run2) => run2.taskId === taskId).sort((left, right) => {
12318
+ if (Number(right.passed) !== Number(left.passed)) {
12319
+ return Number(right.passed) - Number(left.passed);
12320
+ }
12321
+ if (right.qualityScore !== left.qualityScore) {
12322
+ return right.qualityScore - left.qualityScore;
12323
+ }
12324
+ if (right.speedScore !== left.speedScore) {
12325
+ return right.speedScore - left.speedScore;
12326
+ }
12327
+ return (left.latencyMs ?? Number.POSITIVE_INFINITY) - (right.latencyMs ?? Number.POSITIVE_INFINITY);
12328
+ })[0];
12329
+ }
12330
+ function runOfflineTaskEvaluation(inputs, tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
12331
+ const taskMap = new Map(tasks.map((task) => [task.id, task]));
12332
+ const missingTaskIds = Array.from(new Set(inputs.map((input3) => input3.taskId).filter((taskId) => !taskMap.has(taskId))));
12333
+ const runs = inputs.map((input3) => {
12334
+ const task = taskMap.get(input3.taskId);
12335
+ return task ? evaluateRun(task, input3) : void 0;
12336
+ }).filter((run2) => Boolean(run2));
12337
+ const latencies = runs.map((run2) => run2.latencyMs).filter((value) => typeof value === "number");
12338
+ const bestRunsByTask = tasks.map((task) => bestRunForTask(task.id, runs)).filter((run2) => Boolean(run2));
12339
+ return {
12340
+ totalTasks: tasks.length,
12341
+ totalRuns: inputs.length,
12342
+ evaluatedRuns: runs.length,
12343
+ missingTaskIds,
12344
+ passRate: rate2(runs.filter((run2) => run2.passed).length, runs.length),
12345
+ averageQualityScore: average2(runs.map((run2) => run2.qualityScore)),
12346
+ averageSpeedScore: average2(runs.map((run2) => run2.speedScore)),
12347
+ averageLatencyMs: latencies.length ? Number(average2(latencies).toFixed(2)) : 0,
12348
+ averageDimensionScores: averageDimensionScores(runs),
12349
+ bestRunsByTask,
12350
+ byTask: groupRuns(runs, (run2) => run2.taskId),
12351
+ byModel: groupRuns(runs, (run2) => run2.model),
12352
+ runs
12353
+ };
12354
+ }
12355
+ async function runBenchmarkJob(task, model, options) {
12356
+ const startedAt = Date.now();
12357
+ const url = `${options.baseUrl.replace(/\/+$/, "")}/v1/messages`;
12358
+ try {
12359
+ const response = await options.fetchFn(url, {
12360
+ method: "POST",
12361
+ headers: {
12362
+ "Content-Type": "application/json",
12363
+ "anthropic-version": "2023-06-01",
12364
+ ...options.apiKey ? { Authorization: `Bearer ${options.apiKey}`, "x-api-key": options.apiKey } : {}
12365
+ },
12366
+ body: JSON.stringify({
12367
+ model,
12368
+ max_tokens: options.maxTokens,
12369
+ stream: false,
12370
+ metadata: {
12371
+ ctr_eval_task_id: task.id,
12372
+ ctr_eval_intent: task.intent
12373
+ },
12374
+ messages: [
12375
+ {
12376
+ role: "user",
12377
+ content: task.prompt
12378
+ }
12379
+ ]
12380
+ }),
12381
+ ...options.timeoutMs > 0 ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
12382
+ });
12383
+ const latencyMs = Date.now() - startedAt;
12384
+ if (!response.ok) {
12385
+ return {
12386
+ taskId: task.id,
12387
+ model,
12388
+ latencyMs,
12389
+ error: `http_${response.status}`
12390
+ };
12391
+ }
12392
+ const payload = await response.json();
12393
+ return {
12394
+ taskId: task.id,
12395
+ model,
12396
+ latencyMs,
12397
+ output: extractResponseText(payload)
12398
+ };
12399
+ } catch (error) {
12400
+ return {
12401
+ taskId: task.id,
12402
+ model,
12403
+ latencyMs: Date.now() - startedAt,
12404
+ error: error?.name === "TimeoutError" ? "timeout" : error?.message || "request_failed"
12405
+ };
12406
+ }
12407
+ }
12408
+ async function runOfflineTaskBenchmark(options) {
12409
+ const tasks = options.tasks ?? DEFAULT_OFFLINE_EVALUATION_TASKS;
12410
+ const models = options.models.map((model) => model.trim()).filter(Boolean);
12411
+ if (!models.length) {
12412
+ throw new Error("\u81F3\u5C11\u9700\u8981\u63D0\u4F9B\u4E00\u4E2A\u6A21\u578B\u7528\u4E8E\u81EA\u52A8\u8BC4\u6D4B\u3002");
12413
+ }
12414
+ if (!options.baseUrl?.trim()) {
12415
+ throw new Error("\u81EA\u52A8\u8BC4\u6D4B\u9700\u8981 baseUrl\u3002");
12416
+ }
12417
+ const jobs = tasks.flatMap((task) => models.map((model) => ({ task, model })));
12418
+ const inputs = new Array(jobs.length);
12419
+ let nextIndex = 0;
12420
+ const concurrency = Math.max(1, Math.min(Math.floor(options.concurrency ?? 2), 8));
12421
+ const sharedOptions = {
12422
+ baseUrl: options.baseUrl.trim(),
12423
+ apiKey: options.apiKey?.trim() || void 0,
12424
+ timeoutMs: Math.max(0, Math.floor(options.timeoutMs ?? 3e4)),
12425
+ maxTokens: Math.max(1, Math.floor(options.maxTokens ?? 768)),
12426
+ fetchFn: options.fetchFn ?? fetch
12427
+ };
12428
+ async function worker() {
12429
+ while (nextIndex < jobs.length) {
12430
+ const currentIndex = nextIndex;
12431
+ nextIndex += 1;
12432
+ const job = jobs[currentIndex];
12433
+ inputs[currentIndex] = await runBenchmarkJob(job.task, job.model, sharedOptions);
12434
+ }
12435
+ }
12436
+ await Promise.all(Array.from({ length: Math.min(concurrency, jobs.length) }, () => worker()));
12437
+ return {
12438
+ inputs,
12439
+ report: runOfflineTaskEvaluation(inputs, tasks)
12440
+ };
12441
+ }
12442
+ function buildOfflineTaskManifest(tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
12443
+ return {
12444
+ version: 1,
12445
+ description: "Fixed task set for repeatable Claude Trigger Router model-combination evaluation.",
12446
+ tasks: tasks.map((task) => ({
12447
+ id: task.id,
12448
+ intent: task.intent,
12449
+ category: task.category ?? "general",
12450
+ prompt: task.prompt,
12451
+ expectedOutput: task.expectedOutput ?? "A complete answer that satisfies the task prompt.",
12452
+ rubric: {
12453
+ minQualityScore: task.minQualityScore ?? 0.7,
12454
+ minOutputChars: task.minOutputChars ?? 0,
12455
+ maxLatencyMs: task.maxLatencyMs,
12456
+ requiredKeywords: task.requiredKeywords ?? [],
12457
+ forbiddenPatterns: task.forbiddenPatterns ?? [],
12458
+ requiresCodeBlock: Boolean(task.requiresCodeBlock),
12459
+ qualityDimensions: qualityDimensionsForTask(task).map((dimension) => ({
12460
+ id: normalizeDimensionId(dimension.id),
12461
+ label: dimension.label ?? dimension.id,
12462
+ weight: dimension.weight ?? 1,
12463
+ minScore: dimension.minScore ?? 0.7,
12464
+ minOutputChars: dimension.minOutputChars,
12465
+ requiredKeywords: dimension.requiredKeywords ?? [],
12466
+ forbiddenPatterns: dimension.forbiddenPatterns ?? [],
12467
+ requiresCodeBlock: Boolean(dimension.requiresCodeBlock)
12468
+ }))
12469
+ },
12470
+ resultTemplate: {
12471
+ taskId: task.id,
12472
+ model: "<provider,model>",
12473
+ output: "<model output>",
12474
+ latencyMs: 0
12475
+ }
12476
+ }))
12477
+ };
12478
+ }
12479
+ function formatOfflineTaskManifest(tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
12480
+ const lines = [
12481
+ "Offline evaluation tasks",
12482
+ `Total tasks: ${tasks.length}`
12483
+ ];
12484
+ for (const task of tasks) {
12485
+ lines.push(`- ${task.id} [${task.intent}/${task.category ?? "general"}]`);
12486
+ lines.push(` Prompt: ${task.prompt}`);
12487
+ lines.push(` Expected: ${task.expectedOutput ?? "A complete answer that satisfies the task prompt."}`);
12488
+ lines.push(` Rubric: minQuality=${task.minQualityScore ?? 0.7}, minChars=${task.minOutputChars ?? 0}, maxLatencyMs=${task.maxLatencyMs ?? "-"}`);
12489
+ lines.push(` Required: ${(task.requiredKeywords ?? []).join("|") || "-"}`);
12490
+ lines.push(` Forbidden: ${(task.forbiddenPatterns ?? []).join("|") || "-"}`);
12491
+ lines.push(` Requires code block: ${Boolean(task.requiresCodeBlock)}`);
12492
+ lines.push(` Dimensions: ${qualityDimensionsForTask(task).map((dimension) => normalizeDimensionId(dimension.id)).join("|") || "-"}`);
12493
+ }
12494
+ return lines.join("\n");
12495
+ }
12496
+ function formatDimensionSummary(scores) {
12497
+ const entries = Object.entries(scores);
12498
+ if (!entries.length) {
12499
+ return "-";
12500
+ }
12501
+ return entries.map(([id, score]) => `${id}=${score.toFixed(2)}`).join(", ");
12502
+ }
12503
+ function formatOfflineTaskEvaluationReport(report) {
12504
+ const lines = [
12505
+ "Offline routing evaluation",
12506
+ `Tasks: ${report.totalTasks}, runs: ${report.evaluatedRuns}/${report.totalRuns}, passRate: ${(report.passRate * 100).toFixed(1)}%`,
12507
+ `Average quality: ${report.averageQualityScore.toFixed(2)}, speed: ${report.averageSpeedScore.toFixed(2)}, latency: ${report.averageLatencyMs} ms`
12508
+ ];
12509
+ const dimensions = Object.entries(report.averageDimensionScores);
12510
+ if (dimensions.length) {
12511
+ lines.push(`Average dimensions: ${formatDimensionSummary(report.averageDimensionScores)}`);
12512
+ }
12513
+ if (report.missingTaskIds.length) {
12514
+ lines.push(`Missing task ids: ${report.missingTaskIds.join(", ")}`);
12515
+ }
12516
+ lines.push("By model:");
12517
+ for (const item of report.byModel) {
12518
+ lines.push(`- ${item.key}: pass ${(item.passRate * 100).toFixed(1)}%, quality ${item.averageQualityScore.toFixed(2)}, latency ${item.averageLatencyMs} ms, dimensions ${formatDimensionSummary(item.averageDimensionScores)}`);
12519
+ }
12520
+ lines.push("Best runs by task:");
12521
+ for (const run2 of report.bestRunsByTask) {
12522
+ lines.push(`- ${run2.taskId} -> ${run2.model}: ${run2.passed ? "pass" : "fail"}, quality ${run2.qualityScore.toFixed(2)}, latency ${run2.latencyMs ?? "-"} ms`);
12523
+ }
12524
+ const failedRuns = report.runs.filter((run2) => !run2.passed || run2.findings.length);
12525
+ if (failedRuns.length) {
12526
+ lines.push("Findings:");
12527
+ for (const run2 of failedRuns) {
12528
+ lines.push(`- ${run2.taskId} -> ${run2.model}: ${run2.findings.length ? run2.findings.join(", ") : "quality_below_threshold"}`);
12529
+ }
12530
+ }
12531
+ return lines.join("\n");
12532
+ }
12533
+ var DEFAULT_OFFLINE_EVALUATION_TASKS;
12534
+ var init_task_evaluation = __esm({
12535
+ "src/governance/task-evaluation.ts"() {
12536
+ "use strict";
12537
+ DEFAULT_OFFLINE_EVALUATION_TASKS = [
12538
+ {
12539
+ id: "quick_status",
12540
+ intent: "quick_reply",
12541
+ category: "speed",
12542
+ prompt: "Summarize the current service status and next action in two concise sentences.",
12543
+ expectedOutput: "A brief status summary with a concrete next action.",
12544
+ maxLatencyMs: 800,
12545
+ minOutputChars: 40,
12546
+ requiredKeywords: ["status", "next"],
12547
+ forbiddenPatterns: ["TODO", "placeholder", "I cannot"]
12548
+ },
12549
+ {
12550
+ id: "coding_fix",
12551
+ intent: "coding",
12552
+ category: "quality",
12553
+ prompt: "Fix a TypeScript regression and explain the changed behavior with a test plan.",
12554
+ expectedOutput: "A concise fix explanation, a TypeScript code block, and a focused test plan.",
12555
+ maxLatencyMs: 1800,
12556
+ minOutputChars: 120,
12557
+ requiredKeywords: ["fix", "test"],
12558
+ forbiddenPatterns: ["TODO", "...rest of code", "placeholder"],
12559
+ requiresCodeBlock: true
12560
+ },
12561
+ {
12562
+ id: "architecture_review",
12563
+ intent: "architecture",
12564
+ category: "quality",
12565
+ prompt: "Review a router architecture change and list risks, tradeoffs, and rollout checks.",
12566
+ expectedOutput: "A structured architecture review that names risks, tradeoffs, and rollout checks.",
12567
+ maxLatencyMs: 2600,
12568
+ minOutputChars: 160,
12569
+ requiredKeywords: ["risk", "tradeoff", "rollout"],
12570
+ forbiddenPatterns: ["TODO", "placeholder"]
12571
+ },
12572
+ {
12573
+ id: "long_context_triage",
12574
+ intent: "long_context",
12575
+ category: "continuity",
12576
+ prompt: "Triage a long conversation and preserve the user goal, constraints, and open blockers.",
12577
+ expectedOutput: "A continuity-preserving summary with goal, constraints, and blockers.",
12578
+ maxLatencyMs: 3200,
12579
+ minOutputChars: 180,
12580
+ requiredKeywords: ["goal", "constraint", "blocker"],
12581
+ forbiddenPatterns: ["lost context", "cannot access previous"]
12582
+ },
12583
+ {
12584
+ id: "auth_deployment_plan",
12585
+ intent: "security",
12586
+ category: "server_ops",
12587
+ prompt: "Create a safe remote server deployment checklist for an LLM router with API key scope, rotation, audit, and rollback.",
12588
+ expectedOutput: "An operator checklist covering scoped keys, rotation, audit, and rollback.",
12589
+ maxLatencyMs: 2600,
12590
+ minOutputChars: 180,
12591
+ requiredKeywords: ["scope", "rotation", "audit", "rollback"],
12592
+ forbiddenPatterns: ["disable auth", "share the admin key", "placeholder"]
12593
+ },
12594
+ {
12595
+ id: "model_pool_incident",
12596
+ intent: "operations",
12597
+ category: "pool_health",
12598
+ prompt: "Diagnose a model pool incident where one endpoint is slow and another returns intermittent 5xx errors; propose routing actions.",
12599
+ expectedOutput: "A pool health diagnosis with latency, 5xx, fallback or circuit breaker actions.",
12600
+ maxLatencyMs: 2200,
12601
+ minOutputChars: 160,
12602
+ requiredKeywords: ["latency", "5xx", "fallback"],
12603
+ forbiddenPatterns: ["TODO", "placeholder"]
12604
+ }
12605
+ ];
12606
+ }
12607
+ });
12608
+
10719
12609
  // src/cli.ts
10720
12610
  var cli_exports = {};
10721
12611
  __export(cli_exports, {
@@ -10725,7 +12615,7 @@ __export(cli_exports, {
10725
12615
  });
10726
12616
  module.exports = __toCommonJS(cli_exports);
10727
12617
  function getPackageInfo() {
10728
- const content = (0, import_fs9.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
12618
+ const content = (0, import_fs10.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
10729
12619
  const pkg = JSON.parse(content);
10730
12620
  return {
10731
12621
  name: pkg.name ?? "@peterwangze/claude-trigger-router",
@@ -10745,7 +12635,8 @@ function hasArg2(flag, shortFlag) {
10745
12635
  function getArgValue(flag, shortFlag) {
10746
12636
  const args = getArgs();
10747
12637
  const index = args.indexOf(flag) !== -1 ? args.indexOf(flag) : shortFlag ? args.indexOf(shortFlag) : -1;
10748
- return index !== -1 ? args[index + 1] : void 0;
12638
+ const value = index !== -1 ? args[index + 1] : void 0;
12639
+ return value && !value.startsWith("-") ? value : void 0;
10749
12640
  }
10750
12641
  function parsePortValue(portValue, sourceLabel) {
10751
12642
  const trimmed = portValue.trim();
@@ -10765,16 +12656,16 @@ function getPort() {
10765
12656
  }
10766
12657
  try {
10767
12658
  const yaml4 = require("js-yaml");
10768
- if ((0, import_fs9.existsSync)(CONFIG_FILE)) {
10769
- const content = (0, import_fs9.readFileSync)(CONFIG_FILE, "utf-8");
12659
+ if ((0, import_fs10.existsSync)(CONFIG_FILE)) {
12660
+ const content = (0, import_fs10.readFileSync)(CONFIG_FILE, "utf-8");
10770
12661
  const config = yaml4.load(content);
10771
12662
  if (config?.PORT) return config.PORT;
10772
- } else if ((0, import_fs9.existsSync)(CONFIG_FILE_YML)) {
10773
- const content = (0, import_fs9.readFileSync)(CONFIG_FILE_YML, "utf-8");
12663
+ } else if ((0, import_fs10.existsSync)(CONFIG_FILE_YML)) {
12664
+ const content = (0, import_fs10.readFileSync)(CONFIG_FILE_YML, "utf-8");
10774
12665
  const config = yaml4.load(content);
10775
12666
  if (config?.PORT) return config.PORT;
10776
- } else if ((0, import_fs9.existsSync)(CONFIG_FILE_JSON)) {
10777
- const content = (0, import_fs9.readFileSync)(CONFIG_FILE_JSON, "utf-8");
12667
+ } else if ((0, import_fs10.existsSync)(CONFIG_FILE_JSON)) {
12668
+ const content = (0, import_fs10.readFileSync)(CONFIG_FILE_JSON, "utf-8");
10778
12669
  const config = JSON.parse(content);
10779
12670
  if (config?.PORT) return config.PORT;
10780
12671
  }
@@ -10794,6 +12685,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
10794
12685
  \u547D\u4EE4\uFF1A
10795
12686
  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
10796
12687
  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 results.json / --tasks / --run\uFF09
10797
12689
  init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
10798
12690
  deploy \u751F\u6210\u90E8\u7F72\u5165\u53E3\u914D\u7F6E\uFF08\u5F53\u524D\u652F\u6301 deploy init --target server\uFF09
10799
12691
  start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
@@ -10814,6 +12706,9 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
10814
12706
  \u4F7F\u7528\u793A\u4F8B\uFF1A
10815
12707
  ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
10816
12708
  ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
12709
+ ctr eval --tasks # \u67E5\u770B\u56FA\u5B9A\u8BC4\u6D4B\u4EFB\u52A1\u3001prompt \u548C rubric
12710
+ ctr eval --input results.json # \u7528\u56FA\u5B9A\u4EFB\u52A1\u96C6 rubric \u8BC4\u6D4B\u591A\u6A21\u578B\u8F93\u51FA\u7ED3\u679C
12711
+ ctr eval --run --models "sonnet;haiku" # \u81EA\u52A8\u8C03\u7528 CTR /v1/messages \u540E\u8BC4\u6D4B
10817
12712
  ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
10818
12713
  ctr deploy init --target server # \u751F\u6210\u5B89\u5168\u9ED8\u8BA4\u7684 server \u90E8\u7F72\u914D\u7F6E
10819
12714
  ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
@@ -10841,10 +12736,10 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
10841
12736
  function readConfigForCliStatus() {
10842
12737
  const yaml4 = require("js-yaml");
10843
12738
  for (const configFile of [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON]) {
10844
- if (!(0, import_fs9.existsSync)(configFile)) {
12739
+ if (!(0, import_fs10.existsSync)(configFile)) {
10845
12740
  continue;
10846
12741
  }
10847
- const content = (0, import_fs9.readFileSync)(configFile, "utf-8");
12742
+ const content = (0, import_fs10.readFileSync)(configFile, "utf-8");
10848
12743
  return configFile.endsWith(".json") ? JSON.parse(content) : yaml4.load(content);
10849
12744
  }
10850
12745
  return {};
@@ -10905,6 +12800,89 @@ function printRuntimeStatus(config, port, liveInfo) {
10905
12800
  }
10906
12801
  console.log(` \u672C\u5730\u63A5\u5165\uFF1A${clientConnection?.baseUrl || `http://127.0.0.1:${listenerPort}`}`);
10907
12802
  }
12803
+ function readOfflineEvaluationInputs(inputPath) {
12804
+ const payload = JSON.parse((0, import_fs10.readFileSync)(inputPath, "utf-8"));
12805
+ return parseOfflineEvaluationInputs(payload);
12806
+ }
12807
+ function parsePositiveIntegerArg(flag, shortFlag, fallback, label) {
12808
+ const value = getArgValue(flag, shortFlag);
12809
+ if (!value) {
12810
+ return fallback;
12811
+ }
12812
+ if (!/^\d+$/.test(value)) {
12813
+ throw new Error(`${label} \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${value}`);
12814
+ }
12815
+ const parsed = Number.parseInt(value, 10);
12816
+ if (!Number.isInteger(parsed) || parsed < 1) {
12817
+ throw new Error(`${label} \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${value}`);
12818
+ }
12819
+ return parsed;
12820
+ }
12821
+ function parseEvalModelsArg() {
12822
+ const modelsValue = getArgValue("--models") || getArgValue("--model");
12823
+ return (modelsValue ?? "").split(";").map((item) => item.trim()).filter(Boolean);
12824
+ }
12825
+ async function runOfflineEvaluationCli() {
12826
+ if (hasArg2("--tasks")) {
12827
+ if (hasArg2("--json")) {
12828
+ console.log(JSON.stringify(buildOfflineTaskManifest(), null, 2));
12829
+ return;
12830
+ }
12831
+ console.log(formatOfflineTaskManifest());
12832
+ return;
12833
+ }
12834
+ if (hasArg2("--run")) {
12835
+ const models = parseEvalModelsArg();
12836
+ if (!models.length) {
12837
+ console.log('\u8BF7\u63D0\u4F9B\u81EA\u52A8\u8BC4\u6D4B\u6A21\u578B\uFF1Actr eval --run --models "sonnet;haiku"');
12838
+ console.log("\u63D0\u793A\uFF1A\u6A21\u578B\u540D\u4E2D\u53EF\u4EE5\u5305\u542B\u9017\u53F7\uFF0C\u56E0\u6B64\u591A\u4E2A\u6A21\u578B\u7528\u5206\u53F7 ; \u5206\u9694\u3002");
12839
+ process.exit(1);
12840
+ }
12841
+ try {
12842
+ const config = readConfigForCliStatus();
12843
+ const baseUrl = getArgValue("--base-url") || `http://127.0.0.1:${getPort()}`;
12844
+ const apiKey = getArgValue("--api-key") || getLocalClaudeProxyToken(config);
12845
+ const result = await runOfflineTaskBenchmark({
12846
+ models,
12847
+ baseUrl,
12848
+ apiKey,
12849
+ timeoutMs: parsePositiveIntegerArg("--timeout-ms", void 0, 3e4, "timeout-ms"),
12850
+ concurrency: parsePositiveIntegerArg("--concurrency", void 0, 2, "concurrency"),
12851
+ maxTokens: parsePositiveIntegerArg("--max-tokens", void 0, 768, "max-tokens")
12852
+ });
12853
+ if (hasArg2("--json")) {
12854
+ console.log(JSON.stringify(result, null, 2));
12855
+ return;
12856
+ }
12857
+ console.log(formatOfflineTaskEvaluationReport(result.report));
12858
+ return;
12859
+ } catch (error) {
12860
+ console.error(`\u274C \u81EA\u52A8\u8BC4\u6D4B\u5931\u8D25\uFF1A${error.message}`);
12861
+ console.error(' \u793A\u4F8B\uFF1Actr eval --run --models "sonnet;haiku" --base-url http://127.0.0.1:5678');
12862
+ process.exit(1);
12863
+ }
12864
+ }
12865
+ const inputPath = getArgValue("--input", "-i");
12866
+ if (!inputPath) {
12867
+ console.log("\u8BF7\u63D0\u4F9B\u8BC4\u6D4B\u8F93\u5165\u6587\u4EF6\uFF1Actr eval --input results.json");
12868
+ console.log("\u53EF\u5148\u8FD0\u884C\uFF1Actr eval --tasks \u67E5\u770B\u56FA\u5B9A\u4EFB\u52A1\u3001prompt \u548C rubric");
12869
+ console.log('\u8F93\u5165\u683C\u5F0F\uFF1A[{ "taskId": "coding_fix", "model": "provider,model", "output": "...", "latencyMs": 1200 }]');
12870
+ process.exit(1);
12871
+ }
12872
+ try {
12873
+ const inputs = readOfflineEvaluationInputs(inputPath);
12874
+ const report = runOfflineTaskEvaluation(inputs);
12875
+ if (hasArg2("--json")) {
12876
+ console.log(JSON.stringify(report, null, 2));
12877
+ return;
12878
+ }
12879
+ console.log(formatOfflineTaskEvaluationReport(report));
12880
+ } catch (error) {
12881
+ console.error(`\u274C \u79BB\u7EBF\u8BC4\u6D4B\u5931\u8D25\uFF1A${error.message}`);
12882
+ console.error(" \u8BF7\u68C0\u67E5\u8F93\u5165\u683C\u5F0F\uFF1Actr eval --input results.json");
12883
+ process.exit(1);
12884
+ }
12885
+ }
10908
12886
  function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
10909
12887
  try {
10910
12888
  const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
@@ -10996,14 +12974,14 @@ function createBootstrapApiKey() {
10996
12974
  }
10997
12975
  function initConfig2() {
10998
12976
  const force = hasArg2("--force");
10999
- const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
12977
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs10.existsSync);
11000
12978
  if (existingConfig && !force) {
11001
12979
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
11002
12980
  console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
11003
12981
  return;
11004
12982
  }
11005
- if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
11006
- (0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
12983
+ if (!(0, import_fs10.existsSync)(CONFIG_DIR)) {
12984
+ (0, import_fs10.mkdirSync)(CONFIG_DIR, { recursive: true });
11007
12985
  }
11008
12986
  try {
11009
12987
  const yaml4 = require("js-yaml");
@@ -11013,7 +12991,7 @@ function initConfig2() {
11013
12991
  lineWidth: -1,
11014
12992
  noRefs: true
11015
12993
  });
11016
- (0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
12994
+ (0, import_fs10.writeFileSync)(CONFIG_FILE, content, "utf-8");
11017
12995
  const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
11018
12996
  console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
11019
12997
  console.log("");
@@ -11052,14 +13030,14 @@ function initDeployConfig() {
11052
13030
  printDeployHelp();
11053
13031
  process.exit(1);
11054
13032
  }
11055
- const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
13033
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs10.existsSync);
11056
13034
  if (existingConfig && !force) {
11057
13035
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
11058
13036
  console.log(" \u5982\u9700\u8986\u76D6\u90E8\u7F72\u6A21\u677F\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
11059
13037
  return;
11060
13038
  }
11061
- if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
11062
- (0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
13039
+ if (!(0, import_fs10.existsSync)(CONFIG_DIR)) {
13040
+ (0, import_fs10.mkdirSync)(CONFIG_DIR, { recursive: true });
11063
13041
  }
11064
13042
  try {
11065
13043
  const yaml4 = require("js-yaml");
@@ -11071,7 +13049,7 @@ function initDeployConfig() {
11071
13049
  lineWidth: -1,
11072
13050
  noRefs: true
11073
13051
  });
11074
- (0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
13052
+ (0, import_fs10.writeFileSync)(CONFIG_FILE, content, "utf-8");
11075
13053
  const actionLabel = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
11076
13054
  console.log(`\u2705 Server \u90E8\u7F72\u914D\u7F6E${actionLabel}\uFF1A${CONFIG_FILE}`);
11077
13055
  console.log("");
@@ -11299,6 +13277,9 @@ async function main() {
11299
13277
  case "doctor":
11300
13278
  await runDoctorCli();
11301
13279
  break;
13280
+ case "eval":
13281
+ await runOfflineEvaluationCli();
13282
+ break;
11302
13283
  case "init":
11303
13284
  initConfig2();
11304
13285
  break;
@@ -11347,14 +13328,14 @@ async function main() {
11347
13328
  process.exit(command ? 1 : 0);
11348
13329
  }
11349
13330
  }
11350
- var import_child_process3, import_crypto4, import_path8, import_openurl, import_fs9, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
13331
+ var import_child_process3, import_crypto4, import_path9, import_openurl, import_fs10, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
11351
13332
  var init_cli = __esm({
11352
13333
  "src/cli.ts"() {
11353
13334
  import_child_process3 = require("child_process");
11354
13335
  import_crypto4 = require("crypto");
11355
- import_path8 = require("path");
13336
+ import_path9 = require("path");
11356
13337
  import_openurl = __toESM(require("openurl"));
11357
- import_fs9 = require("fs");
13338
+ import_fs10 = require("fs");
11358
13339
  init_index();
11359
13340
  init_processCheck();
11360
13341
  init_constants();
@@ -11364,7 +13345,8 @@ var init_cli = __esm({
11364
13345
  init_doctor();
11365
13346
  init_api_keys();
11366
13347
  init_config();
11367
- PACKAGE_JSON_PATH = (0, import_path8.join)(__dirname, "..", "package.json");
13348
+ init_task_evaluation();
13349
+ PACKAGE_JSON_PATH = (0, import_path9.join)(__dirname, "..", "package.json");
11368
13350
  PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
11369
13351
  PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
11370
13352
  PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";