@peterwangze/claude-trigger-router 1.1.2 → 1.3.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,38 +4093,271 @@ function isRoutedTrace(trace) {
3592
4093
  function isModelSwitch(trace) {
3593
4094
  return Boolean(trace.initialModel && trace.finalModel && trace.initialModel !== trace.finalModel);
3594
4095
  }
3595
- function summarizeRoutingOutcomes(traces) {
3596
- const routedTraces = traces.filter(isRoutedTrace);
3597
- const switchedTraces = traces.filter(isModelSwitch);
3598
- const stableModelCount = traces.filter(
3599
- (trace) => Boolean(trace.initialModel && trace.finalModel && trace.initialModel === trace.finalModel)
3600
- ).length;
3601
- const alignmentOnSwitchCount = switchedTraces.filter((trace) => trace.alignmentUsed).length;
3602
- const cascadeAfterSwitchCount = switchedTraces.filter((trace) => trace.cascadeTriggered).length;
3603
- const switchDistribution = {};
3604
- const routeLatencyValues = {};
3605
- const routeReasonGroups = {};
3606
- const finalModelGroups = {};
3607
- const semanticIntentGroups = {};
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 = {};
3608
4131
  for (const trace of traces) {
3609
- if (isModelSwitch(trace)) {
3610
- const key = `${trace.initialModel} -> ${trace.finalModel}`;
3611
- switchDistribution[key] = {
3612
- from: trace.initialModel,
3613
- to: trace.finalModel,
3614
- count: (switchDistribution[key]?.count ?? 0) + 1
3615
- };
4132
+ const taskKey = getTaskComparisonKey(trace);
4133
+ const model = trace.finalModel;
4134
+ if (!taskKey || !model) {
4135
+ continue;
3616
4136
  }
3617
- if (typeof trace.latencyMs === "number" && Number.isFinite(trace.latencyMs)) {
3618
- for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
3619
- routeLatencyValues[reason] = [...routeLatencyValues[reason] ?? [], trace.latencyMs];
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;
3620
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;
3621
4181
  }
3622
- for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
3623
- addOutcomeGroup(routeReasonGroups, reason, trace);
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;
3624
4211
  }
3625
- addOutcomeGroup(finalModelGroups, trace.finalModel, trace);
3626
- addOutcomeGroup(semanticIntentGroups, trace.semanticIntent, trace);
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
+ }
4327
+ function summarizeRoutingOutcomes(traces) {
4328
+ const routedTraces = traces.filter(isRoutedTrace);
4329
+ const switchedTraces = traces.filter(isModelSwitch);
4330
+ const stableModelCount = traces.filter(
4331
+ (trace) => Boolean(trace.initialModel && trace.finalModel && trace.initialModel === trace.finalModel)
4332
+ ).length;
4333
+ const alignmentOnSwitchCount = switchedTraces.filter((trace) => trace.alignmentUsed).length;
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;
4337
+ const switchDistribution = {};
4338
+ const routeLatencyValues = {};
4339
+ const routeReasonGroups = {};
4340
+ const finalModelGroups = {};
4341
+ const semanticIntentGroups = {};
4342
+ for (const trace of traces) {
4343
+ if (isModelSwitch(trace)) {
4344
+ const key = `${trace.initialModel} -> ${trace.finalModel}`;
4345
+ switchDistribution[key] = {
4346
+ from: trace.initialModel,
4347
+ to: trace.finalModel,
4348
+ count: (switchDistribution[key]?.count ?? 0) + 1
4349
+ };
4350
+ }
4351
+ if (typeof trace.latencyMs === "number" && Number.isFinite(trace.latencyMs)) {
4352
+ for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
4353
+ routeLatencyValues[reason] = [...routeLatencyValues[reason] ?? [], trace.latencyMs];
4354
+ }
4355
+ }
4356
+ for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
4357
+ addOutcomeGroup(routeReasonGroups, reason, trace);
4358
+ }
4359
+ addOutcomeGroup(finalModelGroups, trace.finalModel, trace);
4360
+ addOutcomeGroup(semanticIntentGroups, trace.semanticIntent, trace);
3627
4361
  }
3628
4362
  const averageLatencyByRouteReason = Object.fromEntries(
3629
4363
  Object.entries(routeLatencyValues).sort(([left], [right]) => left.localeCompare(right)).map(([reason, values]) => [reason, average(values)])
@@ -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="subpanel"><div class="row"><strong>Current Router slots</strong><span class="muted">\u89E3\u91CA\u57FA\u7840\u8DEF\u7531\u69FD\u4F4D\u5F15\u7528\u7684 modelId\u3001\u4E0A\u6E38\u6A21\u578B\u3001\u80FD\u529B\u548C\u6F5C\u5728\u914D\u7F6E\u98CE\u9669</span></div><div id="routerSlotSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Configured slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Resolved slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Warnings</span><strong>0</strong></div></div><table id="routerSlotTable" class="management-table"><thead><tr><th>Slot</th><th>When used</th><th>Model ref</th><th>Resolved target</th><th>Capabilities</th><th>Warning</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading router slot explanation...</td></tr></tbody></table><div id="contextWindowGuide" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Context window guide</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u4E0A\u4E0B\u6587\u7A97\u53E3\u4E0E Router.longContext \u5EFA\u8BAE</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div class="alert info"><strong>Models field guide</strong><div class="muted">\u65B0\u914D\u7F6E\u8BF7\u4F7F\u7528\u5165\u53E3\u5B57\u6BB5\uFF1Aid / api / key / interface / model / thinking / metadata\uFF1Bapi_key / api_base_url / protocol \u4EC5\u4F5C\u4E3A\u65E7\u914D\u7F6E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" aria-label="JSON config draft" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4","thinking":"auto","metadata":{"vendor_hint":"openrouter"}}],"Router":{"default":"sonnet"}}'></textarea><div class="muted">JSON \u8349\u7A3F\u540C\u6837\u5EFA\u8BAE\u53EA\u5199\u5165\u53E3\u5B57\u6BB5\uFF1B\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u5F52\u4E00\uFF0C\u65E7\u5B57\u6BB5\u522B\u540D\u65E0\u9700\u624B\u52A8\u8865\u5145\u3002</div><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model pools</strong><span class="muted">Registration.models \u7F16\u8BD1\u51FA\u7684\u540C\u6A21\u578B\u591A\u6E90\u6C60\uFF0C\u5F53\u524D\u652F\u6301 priority / least-latency active endpoint\u3001\u975E\u6D41\u5F0F\u9519\u8BEF fallback\u3001\u5185\u5B58 health/cooldown\u3001\u7194\u65AD\u72B6\u6001\u4E0E\u5EF6\u8FDF\u7A97\u53E3</span></div><table id="compiledModelPoolsTable" class="management-table"><thead><tr><th>Pool</th><th>Strategy</th><th>Active endpoint</th><th>Endpoints</th><th>Warnings</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading model pools...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div id="securitySummary" class="alert info"><strong>Security pending</strong><div class="muted">\u7B49\u5F85\u670D\u52A1\u5B89\u5168\u72B6\u6001\u52A0\u8F7D</div></div><div class="subpanel" id="roleConnectionGuide"><div class="row"><strong>Role & connection guide</strong><span class="muted">\u6309\u5F53\u524D local / server / cloud \u89D2\u8272\u786E\u8BA4\u76D1\u542C\u5730\u5740\u3001\u7EF4\u62A4\u5165\u53E3\u548C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\u65B9\u5F0F\u3002</span></div><div class="scope-guide"><div><strong>current role</strong><span id="roleConnectionSummary" class="muted">${escapedRuntimeMode} / ${escapedServiceRole}</span></div><div><strong>listener</strong><span id="listenerConnectionSummary" class="muted">${escapedListenerSummary}</span></div><div><strong>remote clients</strong><span id="clientConnectionSummary" class="muted">${escapedClientConnectionSummary}</span></div></div><div class="muted" style="margin-top:.75rem">${escapedLocalUserRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedServerMaintainerRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedRemoteClientRoleGuide}</div></div><div class="subpanel" id="authScopeGuide"><div class="row"><strong>Auth scope guide</strong><span class="muted">\u6309\u7528\u9014\u53D1\u653E\u6700\u5C0F\u6743\u9650 key\uFF0C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u4E0D\u8981\u590D\u7528 admin key\u3002</span></div><div class="scope-guide"><div><strong>admin</strong><span class="muted">\u670D\u52A1\u6240\u6709\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001auth \u7BA1\u7406\uFF0C\u4EE5\u53CA\u6240\u6709\u8FD0\u7EF4\u5199\u64CD\u4F5C\u3002</span></div><div><strong>operator</strong><span class="muted">\u65E5\u5E38\u8FD0\u7EF4\u4F7F\u7528\uFF1A\u91CD\u542F\u3001\u6CBB\u7406\u5FEB\u7167\u3001\u5B9A\u65F6\u5FEB\u7167\u3001\u5F02\u5E38\u9608\u503C\u548C\u5F52\u6863\u5220\u9664\uFF1B\u4E0D\u80FD\u67E5\u770B\u914D\u7F6E\u6216\u7BA1\u7406 auth\u3002</span></div><div><strong>client</strong><span class="muted">\u5BA2\u6237\u7AEF\u6A21\u578B\u8C03\u7528\uFF1A/v1/messages\u3001/v1/chat/completions\uFF1B\u6A21\u578B\u8C03\u7528\u914D\u989D\u53EA\u8BA1\u5165\u8FD9\u91CC\u3002</span></div><div><strong>read-only</strong><span class="muted">\u53EA\u8BFB\u89C2\u6D4B\uFF1Ahealth\u3001service-info\u3001compiled models\u3001model pool health\u3001transformers \u548C governance GET\u3002</span></div><div><strong>client + read-only</strong><span class="muted">\u8FDC\u7A0B token \u540C\u65F6\u9700\u8981 ready/status \u63A2\u6D4B\u4E0E\u6A21\u578B\u8C03\u7528\u65F6\u4F7F\u7528\u8BE5\u7EC4\u5408\u3002</span></div></div><div class="muted" style="margin-top:.75rem">\u7BA1\u7406\u5165\u53E3\uFF1A\u7528 admin key \u8C03\u7528 <code>GET /api/auth/keys</code> \u67E5\u770B\u5217\u8868\uFF0C<code>POST /api/auth/keys</code> \u751F\u6210 key\uFF0C<code>POST /api/auth/keys/:id/revoke</code> \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\uFF0C\u8BF7\u76F4\u63A5\u4EA4\u7ED9\u5BF9\u5E94\u5BA2\u6237\u7AEF\u4FDD\u5B58\u3002</div></div><div class="subpanel"><div class="row"><strong>Auth quota</strong><span class="muted">\u6309 managed key \u67E5\u770B\u6A21\u578B\u8C03\u7528\u914D\u989D\u3001\u5F53\u524D\u7528\u91CF\u4E0E\u7A97\u53E3\u91CD\u7F6E\u65F6\u95F4</span></div><table id="authQuotaTable" class="management-table"><thead><tr><th>Key</th><th>Scope</th><th>Status</th><th>Requests</th><th>Tokens</th><th>Window</th></tr></thead><tbody><tr><td colspan="6" class="muted">Waiting for service status...</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Model pool health</strong><span class="muted">\u67E5\u770B\u540C\u6A21\u578B\u591A\u6E90\u6C60\u7684 active endpoint\u3001\u6301\u4E45\u5316\u72B6\u6001\u3001cooldown\u3001\u7194\u65AD\u4E0E\u5EF6\u8FDF\u7A97\u53E3\u3002</span></div><div id="modelPoolHealthSummary" class="alert info"><strong>Pool health pending</strong><div class="muted">\u7B49\u5F85\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001\u52A0\u8F7D</div></div><table id="modelPoolHealthTable" class="management-table"><thead><tr><th>Pool</th><th>Endpoint</th><th>Status</th><th>Latency</th><th>Failures</th><th>Last success</th><th>Recovery</th></tr></thead><tbody><tr><td colspan="7" class="muted">Waiting for model pool health...</td></tr></tbody></table></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/pool-health</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Health</span><strong>-</strong></div><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Model switch rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment on switch</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="healthSummary" class="alert info"><strong>Health pending</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u5065\u5EB7\u6458\u8981\u52A0\u8F7D</div></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Routing tuning</strong><span class="muted">\u57FA\u4E8E outcome \u8BC1\u636E\u7ED9\u51FA SmartRouter \u8C03\u4F18\u5EFA\u8BAE</span></div><ul id="routingTuningList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Quality evidence</strong><span class="muted">\u771F\u5B9E trace \u4E2D\u7684\u5931\u8D25\u3001\u8FDE\u7EED\u6027\u548C\u901F\u5EA6\u98CE\u9669\u6837\u672C</span></div><div id="qualityEvidenceSummary" class="stats"><div class="stat"><span class="muted">Samples</span><strong>-</strong></div><div class="stat"><span class="muted">Risk</span><strong>-</strong></div><div class="stat"><span class="muted">Improvement</span><strong>-</strong></div><div class="stat"><span class="muted">Speed risk</span><strong>-</strong></div></div><ul id="qualityEvidenceList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Task comparison</strong><span class="muted">\u540C\u7C7B\u4EFB\u52A1\u4E0B\u4E0D\u540C\u6700\u7EC8\u6A21\u578B\u7684\u5931\u8D25\u7387\u548C\u901F\u5EA6\u5BF9\u6BD4</span></div><div id="taskComparisonSummary" class="stats"><div class="stat"><span class="muted">Tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Traces</span><strong>-</strong></div></div><ul id="taskComparisonList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Benchmark summary</strong><span class="muted">\u628A\u6CBB\u7406 trace \u4E0E\u56FA\u5B9A\u4EFB\u52A1\u8BC4\u6D4B\u5165\u53E3\u5408\u5E76\u6210\u7EF4\u62A4\u8005 A/B \u95ED\u73AF</span></div><div id="benchmarkSummary" class="stats"><div class="stat"><span class="muted">Comparable tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Evidence samples</span><strong>-</strong></div><div class="stat"><span class="muted">Best quality lift</span><strong>-</strong></div><div class="stat"><span class="muted">Best speed lift</span><strong>-</strong></div></div><ul id="benchmarkActionList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by route</strong><span class="muted">\u5207\u6362\u3001alignment\u3001cascade \u4E0E\u5EF6\u8FDF</span></div><ul id="routeOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by model</strong><span class="muted">\u6700\u7EC8\u6A21\u578B\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="modelOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by intent</strong><span class="muted">\u4EFB\u52A1\u610F\u56FE\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="intentOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\u6458\u8981</li><li><code>GET /api/auth/audit</code> \u2014 \u67E5\u770B\u9274\u6743\u5BA1\u8BA1\u6458\u8981</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const routerSlotSummary=document.getElementById('routerSlotSummary');const routerSlotTableBody=document.querySelector('#routerSlotTable tbody');const contextWindowGuide=document.getElementById('contextWindowGuide');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const listenerStatusSummary=document.getElementById('listenerStatusSummary');const roleConnectionSummary=document.getElementById('roleConnectionSummary');const listenerConnectionSummary=document.getElementById('listenerConnectionSummary');const clientConnectionSummary=document.getElementById('clientConnectionSummary');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const authStatusSummary=document.getElementById('authStatusSummary');const securityStatusSummary=document.getElementById('securityStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const compiledModelPoolsTableBody=document.querySelector('#compiledModelPoolsTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const routeOutcomeRanking=document.getElementById('routeOutcomeRanking');const modelOutcomeRanking=document.getElementById('modelOutcomeRanking');const intentOutcomeRanking=document.getElementById('intentOutcomeRanking');const healthSummary=document.getElementById('healthSummary');const routingTuningList=document.getElementById('routingTuningList');const qualityEvidenceSummary=document.getElementById('qualityEvidenceSummary');const qualityEvidenceList=document.getElementById('qualityEvidenceList');const taskComparisonSummary=document.getElementById('taskComparisonSummary');const taskComparisonList=document.getElementById('taskComparisonList');const benchmarkSummary=document.getElementById('benchmarkSummary');const benchmarkActionList=document.getElementById('benchmarkActionList');const securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable tbody');const modelPoolHealthSummary=document.getElementById('modelPoolHealthSummary');const modelPoolHealthTableBody=document.querySelector('#modelPoolHealthTable tbody');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let lastCompiledModelsData=null;let activeValidationHighlight=null;function withDraftCompiledData(payload){ return { ...(lastCompiledModelsData || {}), normalizedConfig: payload || currentDraftConfig || {} }; }const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&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(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyContextWindowAction(action,modelId){ if(action!=='set-long-context' || !modelId){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u8BE5\u4E0A\u4E0B\u6587\u7A97\u53E3\u64CD\u4F5C'; return; } const payload=buildDraftPayloadFromForm(); payload.Router={ ...(payload.Router || {}), longContext:modelId }; currentDraftConfig=payload; renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06 Router.longContext \u8BBE\u7F6E\u4E3A '+modelId+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function getRouterSlotDefinitions(){ return [ { key:'default', label:'Default', when:'\u666E\u901A\u8BF7\u6C42\u3001\u89C4\u5219\u672A\u547D\u4E2D\u6216\u5176\u4ED6\u69FD\u4F4D\u672A\u914D\u7F6E\u65F6\u4F7F\u7528', required:true }, { key:'think', label:'Thinking', when:'\u8BF7\u6C42\u5305\u542B thinking \u65F6\u4F18\u5148\u4F7F\u7528', required:false }, { key:'longContext', label:'Long context', when:'\u8F93\u5165\u8D85\u8FC7\u9608\u503C\uFF0C\u6216\u5F53\u524D\u6A21\u578B safe_input_tokens \u4E0D\u591F\u65F6\u4F7F\u7528', required:false }, { key:'background', label:'Background', when:'Claude Code \u8F7B\u91CF\u540E\u53F0\u6A21\u578B\u8BF7\u6C42\u65F6\u4F7F\u7528', required:false }, { key:'webSearch', label:'Web search', when:'\u8BF7\u6C42\u5305\u542B web_search \u5DE5\u5177\u65F6\u4F7F\u7528', required:false }, ];}function renderRouterSlotExplanation(data){ const config=data?.normalizedConfig || { Router:(currentDraftConfig.Router && Object.keys(currentDraftConfig.Router).length ? currentDraftConfig.Router : (data?.router || {})) }; const router=config.Router || {}; const modelMap=data?.modelMap || {}; const slots=getRouterSlotDefinitions(); let configured=0; let resolved=0; let warnings=0; const defaultRef=String(router.default || '').trim(); const defaultModel=defaultRef ? modelMap[defaultRef] : null; const rows=slots.map(slot=>{ const ref=String(router[slot.key] || '').trim(); const model=ref ? modelMap[ref] : null; const caps=model?.capabilities || {}; const slotWarnings=[]; if(ref){ configured+=1; } if(ref && model){ resolved+=1; } if(slot.required && !ref){ slotWarnings.push('\u5FC5\u586B\u69FD\u4F4D\u672A\u914D\u7F6E'); } if(ref && !model){ slotWarnings.push('\u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id'); } if(slot.key==='think' && model && caps.thinking?.supported === false){ slotWarnings.push('\u76EE\u6807\u6A21\u578B\u58F0\u660E\u4E0D\u652F\u6301 reasoning'); } if(slot.key==='longContext' && model){ if(!caps.contextWindowTokens){ slotWarnings.push('\u7F3A\u5C11 context_window_tokens'); } if(!caps.safeInputTokens){ slotWarnings.push('\u7F3A\u5C11 safe_input_tokens'); } if(defaultModel?.capabilities?.contextWindowTokens && caps.contextWindowTokens && caps.contextWindowTokens <= defaultModel.capabilities.contextWindowTokens){ slotWarnings.push('\u7A97\u53E3\u4E0D\u9AD8\u4E8E default'); } } if(model && slot.key!=='longContext' && (!caps.contextWindowTokens || !caps.safeInputTokens)){ slotWarnings.push('\u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E'); } warnings+=slotWarnings.length; const target=model ? ('<code>'+esc(model.providerName || '-')+'</code><div class="muted">'+esc(model.modelName || '-')+'</div>') : '<span class="muted">-</span>'; const capabilityParts=model ? [ 'thinking '+(caps.thinking?.supported === false ? 'off' : 'on'), 'tools '+(caps.tools === false ? 'off' : 'on'), 'images '+(caps.images === false ? 'off' : 'on'), caps.contextWindowTokens ? ('ctx '+caps.contextWindowTokens) : 'ctx ?', caps.safeInputTokens ? ('safe '+caps.safeInputTokens) : 'safe ?', ] : []; const warningText=slotWarnings.length ? slotWarnings.join('\uFF1B') : (ref ? 'ok' : '\u672A\u914D\u7F6E\u65F6\u56DE\u5230 default'); const warningClass=slotWarnings.length ? 'warn' : 'info'; return '<tr>' + '<td><strong>'+esc(slot.label)+'</strong><div class="muted">Router.'+esc(slot.key)+'</div></td>' + '<td>'+esc(slot.when)+'</td>' + '<td>'+(ref ? '<code>'+esc(ref)+'</code>' : '<span class="muted">not configured</span>')+'</td>' + '<td>'+target+'</td>' + '<td>'+(capabilityParts.length ? capabilityParts.map(item=>'<span class="pill">'+esc(item)+'</span>').join(' ') : '<span class="muted">-</span>')+'</td>' + '<td><span class="pill '+warningClass+'">'+esc(warningText)+'</span></td>' + '</tr>'; }); routerSlotSummary.innerHTML=[['Configured slots',configured],['Resolved slots',resolved],['Warnings',warnings]].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); routerSlotTableBody.innerHTML=rows.join('');}function readModelMetadataNumber(model,key){ const value=model?.metadata?.[key]; return Number.isFinite(Number(value)) && Number(value)>0 ? Number(value) : undefined;}function getContextWindowEntries(data,config){ const modelMap=data?.modelMap || {}; const draftModels=Array.isArray(config?.Models) ? config.Models : []; if(draftModels.length){ return draftModels.map(model=>{ const id=String(model?.id || '').trim(); const compiled=id ? modelMap[id] : null; const caps=compiled?.capabilities || {}; return { id, modelName:model?.model || compiled?.modelName || '-', contextWindowTokens:readModelMetadataNumber(model,'context_window_tokens') || caps.contextWindowTokens, safeInputTokens:readModelMetadataNumber(model,'safe_input_tokens') || caps.safeInputTokens }; }).filter(item=>item.id); } return Object.entries(modelMap).map(([id,model])=>({ id, modelName:model?.modelName || '-', contextWindowTokens:model?.capabilities?.contextWindowTokens, safeInputTokens:model?.capabilities?.safeInputTokens }));}function renderContextWindowGuide(data){ const config=data?.normalizedConfig || currentDraftConfig || {}; const router=config.Router || {}; const entries=getContextWindowEntries(data,config); if(!entries.length){ contextWindowGuide.innerHTML='<div class="alert info"><strong>Context window guide</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u8FD8\u6CA1\u6709\u53EF\u89E3\u6790\u7684 Models\u3002</div></div>'; return; } const defaultRef=String(router.default || '').trim(); const longRef=String(router.longContext || '').trim(); const defaultEntry=entries.find(item=>item.id===defaultRef); const longEntry=entries.find(item=>item.id===longRef); const ranked=entries.filter(item=>item.contextWindowTokens).sort((a,b)=>(b.contextWindowTokens || 0)-(a.contextWindowTokens || 0)); const best=ranked[0]; const missingCount=entries.filter(item=>!item.contextWindowTokens || !item.safeInputTokens).length; const messages=[]; let level='info'; if(missingCount){ level='warn'; messages.push('\u6709 '+missingCount+' \u4E2A\u6A21\u578B\u7F3A\u5C11 context_window_tokens \u6216 safe_input_tokens\uFF0C\u8D85\u5927\u8BF7\u6C42\u53EF\u80FD\u65E0\u6CD5\u63D0\u524D\u964D\u7EA7/\u5207\u6362\u3002'); } if(entries.length>1 && !longRef){ level='warn'; messages.push('\u591A\u6A21\u578B\u914D\u7F6E\u672A\u8BBE\u7F6E Router.longContext\uFF0C\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u4F1A\u7EE7\u7EED\u4F7F\u7528\u5DF2\u9009\u6A21\u578B\u3002'); } if(longRef && !longEntry){ level='warn'; messages.push('Router.longContext \u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id\u3002'); } if(longEntry && (!longEntry.contextWindowTokens || !longEntry.safeInputTokens)){ level='warn'; messages.push('Router.longContext \u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u6216\u5B89\u5168\u8F93\u5165\u5143\u6570\u636E\u3002'); } if(defaultEntry?.contextWindowTokens && longEntry?.contextWindowTokens && longEntry.contextWindowTokens <= defaultEntry.contextWindowTokens){ level='warn'; messages.push('Router.longContext \u7684\u7A97\u53E3\u4E0D\u9AD8\u4E8E Router.default\uFF0C\u53EF\u80FD\u65E0\u6CD5\u63D0\u5347\u5927\u4E0A\u4E0B\u6587\u4F53\u9A8C\u3002'); } if(!messages.length){ messages.push('\u5F53\u524D\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E\u548C Router.longContext \u914D\u7F6E\u53EF\u7528\u4E8E\u5927\u4E0A\u4E0B\u6587 fallback\u3002'); } const canApplyBest=best?.id && best.id!==longRef && (!defaultEntry?.contextWindowTokens || (best.contextWindowTokens || 0)>defaultEntry.contextWindowTokens); const summaryRows=[['Default', defaultRef || '-'],['Default ctx', defaultEntry?.contextWindowTokens || '?'],['Long context', longRef || '-'],['Long ctx', longEntry?.contextWindowTokens || '?'],['Largest ctx', best ? (best.id+' / '+best.contextWindowTokens) : '-'],['Missing metadata', missingCount]]; contextWindowGuide.innerHTML='<div class="alert '+level+'"><div class="row"><strong>Context window guide</strong>'+(best ? '<span class="pill">largest '+esc(best.id)+'</span>' : '')+'</div><div class="diff-summary">'+summaryRows.map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('')+'</div><ul>'+messages.map(message=>'<li>'+esc(message)+'</li>').join('')+'</ul>'+(canApplyBest ? '<div class="row" style="margin-top:.5rem"><button type="button" data-context-action="set-long-context" data-model-id="'+esc(best.id)+'">\u8BBE\u4E3A Router.longContext</button><span class="muted">'+esc(best.modelName || '')+'</span></div>' : '')+'</div>';}function renderCompiledModels(data){ lastCompiledModelsData=data || null; const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); const modelPoolEntries=Object.entries(data.modelPools || {}); const modelPoolEndpointCount=modelPoolEntries.reduce((sum,[_modelId,pool])=>sum+((pool.endpoints || []).length),0); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); renderRouterSlotExplanation(data); renderContextWindowGuide(data); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04 / '+modelPoolEntries.length+' \u4E2A model pool / '+modelPoolEndpointCount+' \u4E2A pool endpoint'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; compiledModelPoolsTableBody.innerHTML=modelPoolEntries.length ? modelPoolEntries.map(([modelId,pool])=>{ const endpoints=pool.endpoints || []; return '<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td>'+esc(pool.strategy || '-')+'</td>' + '<td><code>'+esc(pool.activeEndpointId || '-')+'</code></td>' + '<td>'+endpoints.map(endpoint=>{ const latency=endpoint.health?.latency; return '<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+' / '+esc(endpoint.health?.status || 'healthy')+(latency ? ' / avg '+esc(Math.round(latency.averageMs))+'ms' : '')+'</span><div class="muted">'+esc(endpoint.upstreamServiceId || endpoint.api || '-')+'</div></div>'; }).join('')+'</td>' + '<td>'+((pool.warnings || []).length ? pool.warnings.map(w=>'<div class="warning-text">'+esc(w)+'</div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No compiled model pools</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(currentDraftConfig)); renderContextWindowGuide(withDraftCompiledData(currentDraftConfig)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderContextWindowGuide(lastCompiledModelsData); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderContextWindowGuide(withDraftCompiledData(payload)); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [serviceRes,remoteRes]=await Promise.all([fetch('/api/service-info'),fetch('/api/remote-status')]); const data=await serviceRes.json(); const remoteData=await remoteRes.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; serviceModeStatus.textContent=data.runtimeMode || '-'; serviceRoleStatus.textContent=data.serviceRole || '-'; renderRoleConnectionGuide(data); const auth=data.auth || {}; const managed=auth.managedKeys || {}; const quota=auth.quota || {}; const quotaText=Number.isFinite(quota.requestsUsed) ? (' \xB7 quota '+quota.requestsUsed+' req'+(quota.windowResetAt ? ' \xB7 reset '+String(quota.windowResetAt).replace('T',' ').replace('.000Z','Z') : '')) : ''; authStatusSummary.textContent=auth.required ? ((auth.bootstrapConfigured ? 'bootstrap' : 'managed')+' \xB7 '+(managed.active ?? 0)+' active'+quotaText) : 'not configured'; renderAuthQuotaTable(quota); const security=data.security || {}; const issues=Array.isArray(security.issues) ? security.issues : []; securityStatusSummary.textContent=security.status || '-'; securitySummary.className='alert '+((security.status === 'critical') ? 'critical' : (security.status === 'warning' ? 'warn' : 'info')); securitySummary.innerHTML='<strong>Security: '+esc(security.status || '-')+'</strong><div>'+esc(issues[0]?.message || '\u5F53\u524D\u670D\u52A1\u672A\u53D1\u73B0\u660E\u663E\u9274\u6743\u66B4\u9732\u98CE\u9669')+'</div>'+ (issues.length ? '<ul class="mini-list">'+issues.map(issue=>'<li>'+esc(issue.action || issue.code)+'</li>').join('')+'</ul>' : ''); const registration=data.registration || {}; registrationStatusSummary.textContent=registration.enabled ? ((registration.models ?? 0)+' models / '+(registration.upstreamServices ?? 0)+' upstream') : 'disabled'; const remote=remoteData.remote || {}; remoteStatusSummary.textContent=remote.enabled ? ((remote.ready ? 'ready' : (remote.reachable ? 'reachable' : 'unreachable'))+' \xB7 '+(remote.baseUrl || '-')) : 'disabled'; const remoteRegistration=remoteData.remoteRegistration || {}; const remoteRegistrationSummary=remoteRegistration.summary || {}; remoteRegistrationStatusSummary.textContent=remoteRegistration.enabled ? (remoteRegistration.available ? (remoteRegistration.registrationEnabled ? ((remoteRegistrationSummary.models ?? 0)+' remote models / '+(remoteRegistrationSummary.upstreamServices ?? 0)+' upstream') : 'remote registration disabled') : ('unavailable \xB7 '+(remoteRegistration.error || remoteRegistration.baseUrl || '-'))) : 'disabled'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } try { await loadModelPoolHealth(); } catch (_poolError) { modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderContextWindowGuide(lastCompiledModelsData); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function 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); });contextWindowGuide.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-context-action]'); if(!btn){ return; } applyContextWindowAction(btn.dataset.contextAction, btn.dataset.modelId); });healthSummary.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-health-action]'); if(btn){ applyHealthAction(btn.dataset.healthAction); } });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics,health,outcome){ metricsGrid.innerHTML=[ ['Health', health?.status || 'idle'], ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Model switch rate', pct(outcome?.modelSwitchRate)], ['Alignment on switch', pct(outcome?.alignmentOnSwitchRate)], ['Context fallback', pct(outcome?.contextWindowFallbackRate)], ['Context exceeded', pct(outcome?.contextWindowExceededRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderOutcomeGroups(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code><span class="muted"> \xB7 '+esc(item.totalTraces)+' traces</span></span><strong>switch '+esc(pct(item.modelSwitchRate))+' \xB7 align '+esc(pct(item.alignmentOnSwitchRate))+' \xB7 cascade '+esc(pct(item.cascadeAfterSwitchRate))+' \xB7 '+esc(fmt(item.averageLatencyMs))+' ms</strong></li>').join('');}function renderRoutingTuning(items){ if(!items || !items.length){ routingTuningList.innerHTML='<li><span class="muted">No routing tuning recommendations</span><strong>healthy</strong></li>'; return; } routingTuningList.innerHTML=items.map(item=>'<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 renderBenchmarkSummary(taskComparison,qualityEvidence){ const bestQuality=taskComparison?.bestQualityLiftTask; const bestSpeed=taskComparison?.bestSpeedLiftTask; benchmarkSummary.innerHTML=[ ['Comparable tasks',taskComparison?.totalComparedTasks || 0], ['Evidence samples',qualityEvidence?.totalSamples || 0], ['Best quality lift',bestQuality ? pct(bestQuality.failureRateDelta || 0) : '-'], ['Best speed lift',bestSpeed ? (fmt(bestSpeed.latencyDeltaMs || 0)+' ms') : '-'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const actions=[]; if((taskComparison?.totalComparedTasks || 0)===0){ actions.push(['Collect comparable traces','Send the same task class through at least two final models, then refresh metrics.']); } if((qualityEvidence?.totalSamples || 0)===0){ actions.push(['Collect quality evidence','Enable cascade, shadow, context-window or model-pool signals so routing wins and risks become visible.']); } actions.push(['Run fixed benchmark','ctr eval --tasks && ctr eval --run --models "sonnet;haiku" --json']); actions.push(['Add calibration','Attach humanScore or judgeScore to ctr eval input results before treating rubric scores as release evidence.']); benchmarkActionList.innerHTML=actions.map(([title,detail])=>'<li><span><strong>'+esc(title)+'</strong><div class="muted">'+esc(detail)+'</div></span><strong>benchmark</strong></li>').join('');}function renderAnomalies(anomalies,health){ const status=health?.status || 'idle'; const message=health?.message || 'No governance traces yet.'; const actions=Array.isArray(health?.actions) ? health.actions : []; healthSummary.className='alert '+esc(status === 'critical' ? 'critical' : (status === 'watch' ? 'warn' : 'info')); healthSummary.innerHTML='<strong>Health: '+esc(status)+'</strong><div>'+esc(message)+'</div>'+ (actions.length ? '<ul class="mini-list">'+actions.map(action=>'<li><button type="button" data-health-action="'+esc(action)+'">'+esc(action)+'</button></li>').join('')+'</ul>' : ''); if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function applyHealthAction(action){ const text=String(action || '').toLowerCase(); const routeReasonInput=document.getElementById('routeReason'); const cascadeSelect=document.getElementById('cascadeTriggered'); const shadowSelect=document.getElementById('shadowChecked'); if(text.includes('cascade')){ cascadeSelect.value='true'; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered cascade traces'; } else if(text.includes('shadow')){ shadowSelect.value='true'; cascadeSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered shadow traces'; } else { cascadeSelect.value=''; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: showing recent traces'; } loadTraces(); document.getElementById('traceTable').scrollIntoView({ behavior:'smooth', block:'start' });}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes,healthRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query), fetch('/api/governance/health'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); const healthData=await healthRes.json(); const health=healthData.health || metricsData.health; renderMetrics(metricsData.metrics || {},health,metricsData.outcome || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRoutingTuning(health?.routingTuning || []); renderQualityEvidence(metricsData.qualityEvidence || {}); renderTaskComparison(metricsData.taskComparison || {}); renderBenchmarkSummary(metricsData.taskComparison || {},metricsData.qualityEvidence || {}); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderOutcomeGroups(routeOutcomeRanking,metricsData.outcome?.byRouteReason || [],'No route outcomes'); renderOutcomeGroups(modelOutcomeRanking,metricsData.outcome?.byFinalModel || [],'No model outcomes'); renderOutcomeGroups(intentOutcomeRanking,metricsData.outcome?.bySemanticIntent || [],'No intent outcomes'); renderTrendTable(metricsData || {}); const traces=data.traces || []; 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();
@@ -5026,6 +5969,16 @@ var init_server = __esm({
5026
5969
  createServer = (config) => {
5027
5970
  const server = new import_llms.default(config);
5028
5971
  const configuredThresholds = config.initialConfig?.Governance?.observability?.anomaly_thresholds ?? {};
5972
+ const readActiveConfig = async () => {
5973
+ try {
5974
+ const currentConfig = await readConfigFile();
5975
+ if (currentConfig && typeof currentConfig === "object" && Object.keys(currentConfig).length > 0) {
5976
+ return currentConfig;
5977
+ }
5978
+ } catch {
5979
+ }
5980
+ return config.initialConfig ?? {};
5981
+ };
5029
5982
  const readGovernanceMetricsQuery = (query) => {
5030
5983
  const limit = query?.limit ? Number(query.limit) : void 0;
5031
5984
  const windowMs = query?.windowMs ? Number(query.windowMs) : void 0;
@@ -5060,12 +6013,13 @@ var init_server = __esm({
5060
6013
  return buildDraftConfigView(await readConfigFile());
5061
6014
  });
5062
6015
  server.app.get("/api/models/compiled", async () => {
5063
- const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
6016
+ const normalizedResult = normalizeAndValidateConfig(await readActiveConfig());
5064
6017
  const normalized = normalizedResult.config;
5065
6018
  const compiled = toCompiledRegistryView(normalized);
5066
6019
  const capabilityWarnings = collectCapabilityWarnings(normalized);
5067
6020
  return {
5068
6021
  ...compiled,
6022
+ router: normalized.Router ?? {},
5069
6023
  capabilityWarnings,
5070
6024
  warnings: normalizedResult.warnings,
5071
6025
  issueReport: buildValidationIssueReport({
@@ -5075,6 +6029,9 @@ var init_server = __esm({
5075
6029
  })
5076
6030
  };
5077
6031
  });
6032
+ server.app.get("/api/models/pool-health", async () => {
6033
+ return buildModelPoolHealthReport(await readActiveConfig());
6034
+ });
5078
6035
  server.app.post("/api/models/compiled/preview", async (req, reply) => {
5079
6036
  const rawConfig = req.body ?? {};
5080
6037
  let rawCompiled = null;
@@ -5101,7 +6058,7 @@ var init_server = __esm({
5101
6058
  })
5102
6059
  };
5103
6060
  }
5104
- const currentCompiled = toCompiledRegistryView(config.initialConfig ?? {});
6061
+ const currentCompiled = toCompiledRegistryView(await readActiveConfig());
5105
6062
  const previewCompiled = toCompiledRegistryView(result.config);
5106
6063
  const previewCapabilityWarnings = collectCapabilityWarnings(result.config);
5107
6064
  return {
@@ -5274,13 +6231,17 @@ var init_server = __esm({
5274
6231
  server.app.get("/api/remote-status", async (req) => {
5275
6232
  const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
5276
6233
  const normalized = normalizedResult.config;
5277
- const remote = await probeRemoteServiceStatus(normalized.Runtime?.remote_service);
6234
+ const [remote, remoteRegistration] = await Promise.all([
6235
+ probeRemoteServiceStatus(normalized.Runtime?.remote_service),
6236
+ probeRemoteRegistrationStatus(normalized.Runtime?.remote_service)
6237
+ ]);
5278
6238
  const governanceReport = getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query ?? {}));
5279
6239
  return {
5280
6240
  service: SERVICE_NAME,
5281
6241
  ready: true,
5282
6242
  runtimeMode: normalized.Runtime?.mode ?? "local",
5283
6243
  remote,
6244
+ remoteRegistration,
5284
6245
  compiledModels: summarizeCompiledModels(normalized),
5285
6246
  governance: summarizeGovernanceAlerts(governanceReport),
5286
6247
  issueReport: buildValidationIssueReport({
@@ -5510,8 +6471,8 @@ var init_server = __esm({
5510
6471
  reply.send({ success: true, message: "Service restart initiated" });
5511
6472
  setTimeout(() => {
5512
6473
  const { spawn: spawn3 } = require("child_process");
5513
- const { join: join9 } = require("path");
5514
- const cliPath = join9(__dirname, "cli.js");
6474
+ const { join: join10 } = require("path");
6475
+ const cliPath = join10(__dirname, "cli.js");
5515
6476
  const currentPort = config.initialConfig?.PORT;
5516
6477
  const restartArgs = [cliPath, "start", "--daemon"];
5517
6478
  if (currentPort) {
@@ -5534,7 +6495,85 @@ var init_server = __esm({
5534
6495
  });
5535
6496
 
5536
6497
  // src/router/index.ts
5537
- var import_tiktoken, enc, calculateTokenCount, getUseModel, applyModelThinking, router;
6498
+ function evaluateContextFit(compiled, req, tokenCount) {
6499
+ const safeInputTokens = compiled?.capabilities?.safeInputTokens;
6500
+ const contextWindowTokens = compiled?.capabilities?.contextWindowTokens;
6501
+ const outputTokens = getRequestedOutputTokens(req.body);
6502
+ const thinkingTokens = getEffectiveThinkingBudgetTokens(compiled, req.body);
6503
+ const estimatedTotalTokens = tokenCount + outputTokens + thinkingTokens;
6504
+ if (safeInputTokens && tokenCount > safeInputTokens) {
6505
+ return {
6506
+ fits: false,
6507
+ code: "safe_input_exceeded",
6508
+ inputTokens: tokenCount,
6509
+ estimatedTotalTokens,
6510
+ limit: safeInputTokens
6511
+ };
6512
+ }
6513
+ if (contextWindowTokens && estimatedTotalTokens > contextWindowTokens) {
6514
+ return {
6515
+ fits: false,
6516
+ code: "context_window_exceeded",
6517
+ inputTokens: tokenCount,
6518
+ estimatedTotalTokens,
6519
+ limit: contextWindowTokens
6520
+ };
6521
+ }
6522
+ return {
6523
+ fits: true,
6524
+ inputTokens: tokenCount,
6525
+ estimatedTotalTokens
6526
+ };
6527
+ }
6528
+ function applyContextWindowGuard(req, config, selectedModel, tokenCount) {
6529
+ if (!selectedModel) {
6530
+ return selectedModel;
6531
+ }
6532
+ const selectedCompiled = getCompiledModelRef(config, selectedModel);
6533
+ const selectedFit = evaluateContextFit(selectedCompiled, req, tokenCount);
6534
+ if (selectedFit.fits) {
6535
+ return selectedModel;
6536
+ }
6537
+ const longContextModel = config.Router.longContext ? resolveModelReference(config, config.Router.longContext) : void 0;
6538
+ if (longContextModel && longContextModel !== selectedModel) {
6539
+ const longContextCompiled = getCompiledModelRef(config, longContextModel);
6540
+ const longContextFit = evaluateContextFit(longContextCompiled, req, tokenCount);
6541
+ if (longContextFit.fits) {
6542
+ log(
6543
+ "Using long context model due to selected model context capacity:",
6544
+ selectedModel,
6545
+ "->",
6546
+ longContextModel,
6547
+ "input tokens:",
6548
+ selectedFit.inputTokens,
6549
+ "estimated total tokens:",
6550
+ selectedFit.estimatedTotalTokens,
6551
+ "limit:",
6552
+ selectedFit.limit
6553
+ );
6554
+ if (req.governanceTrace) {
6555
+ appendTraceReason(
6556
+ req.governanceTrace,
6557
+ `context_window_fallback:${selectedCompiled?.id ?? selectedModel}->${longContextCompiled?.id ?? longContextModel}`
6558
+ );
6559
+ }
6560
+ return longContextModel;
6561
+ }
6562
+ }
6563
+ req.contextWindowExceeded = {
6564
+ code: selectedFit.code,
6565
+ model: selectedCompiled?.id ?? selectedModel,
6566
+ inputTokens: selectedFit.inputTokens,
6567
+ estimatedTotalTokens: selectedFit.estimatedTotalTokens,
6568
+ limit: selectedFit.limit,
6569
+ longContextModel: config.Router.longContext
6570
+ };
6571
+ if (req.governanceTrace) {
6572
+ appendTraceReason(req.governanceTrace, `context_window_exceeded:${selectedCompiled?.id ?? selectedModel}`);
6573
+ }
6574
+ return selectedModel;
6575
+ }
6576
+ var import_tiktoken, enc, calculateTokenCount, getUseModel, applyModelThinking, readPositiveInteger, getRequestedOutputTokens, getThinkingBudgetTokens, getEffectiveThinkingBudgetTokens, router;
5538
6577
  var init_router = __esm({
5539
6578
  "src/router/index.ts"() {
5540
6579
  "use strict";
@@ -5662,6 +6701,22 @@ var init_router = __esm({
5662
6701
  req.body.thinking.budget_tokens = thinking.budget_tokens;
5663
6702
  }
5664
6703
  };
6704
+ readPositiveInteger = (value) => {
6705
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
6706
+ };
6707
+ getRequestedOutputTokens = (body) => {
6708
+ return readPositiveInteger(body?.max_tokens) ?? readPositiveInteger(body?.max_completion_tokens) ?? 0;
6709
+ };
6710
+ getThinkingBudgetTokens = (body) => {
6711
+ return readPositiveInteger(body?.thinking?.budget_tokens) ?? 0;
6712
+ };
6713
+ getEffectiveThinkingBudgetTokens = (compiled, body) => {
6714
+ const modelThinking = compiled?.thinking;
6715
+ if (modelThinking?.mode === "off") {
6716
+ return 0;
6717
+ }
6718
+ return readPositiveInteger(modelThinking?.budget_tokens) ?? getThinkingBudgetTokens(body);
6719
+ };
5665
6720
  router = async (req, _res, context) => {
5666
6721
  const { config, event: event2 } = context;
5667
6722
  if (req.body.metadata?.user_id) {
@@ -5693,7 +6748,7 @@ var init_router = __esm({
5693
6748
  if (!model && !req.body.model.includes(",")) {
5694
6749
  model = await getUseModel(req, tokenCount, config, lastMessageUsage);
5695
6750
  }
5696
- req.body.model = model ?? req.body.model;
6751
+ req.body.model = applyContextWindowGuard(req, config, model ?? req.body.model, tokenCount);
5697
6752
  applyModelThinking(req, config, req.body.model);
5698
6753
  const compiledModel = getCompiledModelRef(config, req.body.model);
5699
6754
  if (compiledModel?.source === "registration" && compiledModel.modelPool) {
@@ -5733,6 +6788,7 @@ function authRequirementForRequest(req) {
5733
6788
  "/api/remote-status",
5734
6789
  "/api/registration",
5735
6790
  "/api/models/compiled",
6791
+ "/api/models/pool-health",
5736
6792
  "/api/transformers",
5737
6793
  "/api/governance/health",
5738
6794
  "/api/governance/metrics",
@@ -5743,12 +6799,21 @@ function authRequirementForRequest(req) {
5743
6799
  "/v1/messages",
5744
6800
  "/v1/chat/completions"
5745
6801
  ]);
6802
+ const operatorWritePaths = /* @__PURE__ */ new Set([
6803
+ "/api/restart",
6804
+ "/api/governance/metrics/snapshots",
6805
+ "/api/governance/metrics/schedules",
6806
+ "/api/governance/observability/anomaly-thresholds"
6807
+ ]);
5746
6808
  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
6809
  return "read-only";
5748
6810
  }
5749
6811
  if (modelCallPaths.has(path)) {
5750
6812
  return "client";
5751
6813
  }
6814
+ if (method === "POST" && (operatorWritePaths.has(path) || path.startsWith("/api/governance/archives/") && path.endsWith("/delete"))) {
6815
+ return "operator";
6816
+ }
5752
6817
  return path.startsWith("/api/") || path === "/ui" ? "admin" : "client";
5753
6818
  }
5754
6819
  function isQuotaMeteredRequest(req) {
@@ -5852,7 +6917,7 @@ async function loadPersistedAuthQuotaUsage() {
5852
6917
  if (!(0, import_fs4.existsSync)(QUOTA_USAGE_FILE)) {
5853
6918
  return void 0;
5854
6919
  }
5855
- const content = await (0, import_promises2.readFile)(QUOTA_USAGE_FILE, "utf-8");
6920
+ const content = await (0, import_promises3.readFile)(QUOTA_USAGE_FILE, "utf-8");
5856
6921
  const parsed = JSON.parse(content);
5857
6922
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5858
6923
  }
@@ -5862,17 +6927,17 @@ async function savePersistedAuthQuotaUsage(usage) {
5862
6927
  }
5863
6928
  const tempFile = `${QUOTA_USAGE_FILE}.tmp`;
5864
6929
  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);
6930
+ await (0, import_promises3.writeFile)(tempFile, JSON.stringify(usage, null, 2), "utf-8");
6931
+ await (0, import_promises3.rename)(tempFile, QUOTA_USAGE_FILE);
5867
6932
  });
5868
6933
  await quotaUsageWriteQueue;
5869
6934
  }
5870
- var import_fs4, import_promises2, import_path5, QUOTA_USAGE_FILE, quotaUsageWriteQueue;
6935
+ var import_fs4, import_promises3, import_path5, QUOTA_USAGE_FILE, quotaUsageWriteQueue;
5871
6936
  var init_quota_persistence = __esm({
5872
6937
  "src/auth/quota-persistence.ts"() {
5873
6938
  "use strict";
5874
6939
  import_fs4 = require("fs");
5875
- import_promises2 = require("fs/promises");
6940
+ import_promises3 = require("fs/promises");
5876
6941
  import_path5 = require("path");
5877
6942
  init_constants();
5878
6943
  QUOTA_USAGE_FILE = (0, import_path5.join)(HOME_DIR, "auth-quota-usage.json");
@@ -7450,6 +8515,92 @@ var init_trigger = __esm({
7450
8515
  }
7451
8516
  });
7452
8517
 
8518
+ // src/models/pool-health-persistence.ts
8519
+ async function loadPersistedModelPoolHealth() {
8520
+ if (!(0, import_fs6.existsSync)(MODEL_POOL_HEALTH_FILE)) {
8521
+ return void 0;
8522
+ }
8523
+ const content = await (0, import_promises4.readFile)(MODEL_POOL_HEALTH_FILE, "utf-8");
8524
+ const parsed = JSON.parse(content);
8525
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
8526
+ }
8527
+ async function savePersistedModelPoolHealth(payload) {
8528
+ if (!(0, import_fs6.existsSync)(CONFIG_DIR)) {
8529
+ (0, import_fs6.mkdirSync)(CONFIG_DIR, { recursive: true });
8530
+ }
8531
+ const tempFile = `${MODEL_POOL_HEALTH_FILE}.tmp`;
8532
+ modelPoolHealthWriteQueue = modelPoolHealthWriteQueue.catch(() => void 0).then(async () => {
8533
+ await (0, import_promises4.writeFile)(tempFile, JSON.stringify(payload, null, 2), "utf-8");
8534
+ await (0, import_promises4.rename)(tempFile, MODEL_POOL_HEALTH_FILE);
8535
+ });
8536
+ await modelPoolHealthWriteQueue;
8537
+ }
8538
+ function createModelPoolHealthPersistenceScheduler(options = {}) {
8539
+ const debounceMs = options.debounceMs ?? 25;
8540
+ const save = options.save ?? savePersistedModelPoolHealth;
8541
+ const onError = options.onError;
8542
+ let pendingPayload;
8543
+ let persistTimer;
8544
+ let persistQueue = Promise.resolve();
8545
+ const enqueue = (payload) => {
8546
+ persistQueue = persistQueue.catch(() => void 0).then(async () => {
8547
+ try {
8548
+ await save(payload);
8549
+ } catch (error) {
8550
+ onError?.(error);
8551
+ }
8552
+ });
8553
+ return persistQueue;
8554
+ };
8555
+ const flush = async (payload) => {
8556
+ if (payload) {
8557
+ pendingPayload = payload;
8558
+ }
8559
+ if (persistTimer) {
8560
+ clearTimeout(persistTimer);
8561
+ persistTimer = void 0;
8562
+ }
8563
+ const nextPayload = pendingPayload;
8564
+ pendingPayload = void 0;
8565
+ if (nextPayload) {
8566
+ await enqueue(nextPayload);
8567
+ return;
8568
+ }
8569
+ await persistQueue;
8570
+ };
8571
+ const schedule = (payload) => {
8572
+ pendingPayload = payload;
8573
+ if (persistTimer) {
8574
+ return;
8575
+ }
8576
+ persistTimer = setTimeout(() => {
8577
+ persistTimer = void 0;
8578
+ const nextPayload = pendingPayload;
8579
+ pendingPayload = void 0;
8580
+ if (nextPayload) {
8581
+ void enqueue(nextPayload);
8582
+ }
8583
+ }, debounceMs);
8584
+ persistTimer.unref?.();
8585
+ };
8586
+ return {
8587
+ schedule,
8588
+ flush
8589
+ };
8590
+ }
8591
+ var import_fs6, import_promises4, import_path6, MODEL_POOL_HEALTH_FILE, modelPoolHealthWriteQueue;
8592
+ var init_pool_health_persistence = __esm({
8593
+ "src/models/pool-health-persistence.ts"() {
8594
+ "use strict";
8595
+ import_fs6 = require("fs");
8596
+ import_promises4 = require("fs/promises");
8597
+ import_path6 = require("path");
8598
+ init_constants();
8599
+ MODEL_POOL_HEALTH_FILE = (0, import_path6.join)(CONFIG_DIR, "model-pool-health.json");
8600
+ modelPoolHealthWriteQueue = Promise.resolve();
8601
+ }
8602
+ });
8603
+
7453
8604
  // src/protocols/openai.ts
7454
8605
  function toOpenAIContent(parts) {
7455
8606
  const contentParts = parts.filter(
@@ -7800,8 +8951,8 @@ function cloneRequestBody(value) {
7800
8951
  }
7801
8952
  async function initializeClaudeConfig() {
7802
8953
  const homeDir = (0, import_os2.homedir)();
7803
- const configPath = (0, import_path6.join)(homeDir, ".claude.json");
7804
- if (!(0, import_fs6.existsSync)(configPath)) {
8954
+ const configPath = (0, import_path7.join)(homeDir, ".claude.json");
8955
+ if (!(0, import_fs7.existsSync)(configPath)) {
7805
8956
  log(`Creating ${configPath} for Claude Code compatibility (onboarding bypass)`);
7806
8957
  const userID = Array.from(
7807
8958
  { length: 64 },
@@ -7815,7 +8966,7 @@ async function initializeClaudeConfig() {
7815
8966
  lastOnboardingVersion: "1.0.17",
7816
8967
  projects: {}
7817
8968
  };
7818
- await (0, import_promises3.writeFile)(configPath, JSON.stringify(configContent, null, 2));
8969
+ await (0, import_promises5.writeFile)(configPath, JSON.stringify(configContent, null, 2));
7819
8970
  }
7820
8971
  }
7821
8972
  function buildServerInitialConfig(config, registry, host, servicePort) {
@@ -7824,13 +8975,97 @@ function buildServerInitialConfig(config, registry, host, servicePort) {
7824
8975
  providers: registry.providers,
7825
8976
  HOST: host,
7826
8977
  PORT: servicePort,
7827
- LOG_FILE: (0, import_path6.join)(
8978
+ LOG_FILE: (0, import_path7.join)(
7828
8979
  (0, import_os2.homedir)(),
7829
8980
  ".claude-trigger-router",
7830
8981
  "claude-trigger-router.log"
7831
8982
  )
7832
8983
  };
7833
8984
  }
8985
+ function isRemoteForwardEnabled(config) {
8986
+ const runtime = config?.Runtime ?? {};
8987
+ const remoteService = runtime.remote_service ?? {};
8988
+ return (runtime.mode ?? "local") === "local" && Boolean(remoteService.enabled) && typeof remoteService.base_url === "string" && remoteService.base_url.trim().length > 0;
8989
+ }
8990
+ function isModelCallPath(url) {
8991
+ const path = String(url ?? "").split("?")[0];
8992
+ return path === "/v1/messages" || path === "/v1/chat/completions";
8993
+ }
8994
+ function getRemoteForwardPath(url) {
8995
+ const requestUrl = String(url ?? "");
8996
+ return requestUrl.startsWith("/") ? requestUrl : `/${requestUrl}`;
8997
+ }
8998
+ function buildRemoteForwardHeaders(req, authToken) {
8999
+ const headers = {};
9000
+ const passThroughHeaders = [
9001
+ "accept",
9002
+ "anthropic-version",
9003
+ "anthropic-beta",
9004
+ "content-type"
9005
+ ];
9006
+ for (const header of passThroughHeaders) {
9007
+ const value = req.headers?.[header];
9008
+ if (typeof value === "string" && value.trim()) {
9009
+ headers[header] = value;
9010
+ }
9011
+ }
9012
+ if (!headers["content-type"]) {
9013
+ headers["content-type"] = "application/json";
9014
+ }
9015
+ if (authToken?.trim()) {
9016
+ headers.Authorization = `Bearer ${authToken.trim()}`;
9017
+ }
9018
+ headers["x-ctr-remote-forward"] = "1";
9019
+ return headers;
9020
+ }
9021
+ async function forwardModelCallToRemote(req, reply, config) {
9022
+ if (!isModelCallPath(req.url) || !isRemoteForwardEnabled(config) || req.headers?.["x-ctr-remote-forward"] === "1") {
9023
+ return false;
9024
+ }
9025
+ const remoteService = config.Runtime.remote_service;
9026
+ const remoteBaseUrl = remoteService.base_url.trim().replace(/\/+$/, "");
9027
+ const forwardPath = getRemoteForwardPath(req.url);
9028
+ const path = forwardPath.split("?")[0];
9029
+ const targetUrl = `${remoteBaseUrl}${forwardPath}`;
9030
+ try {
9031
+ const response = await fetch(targetUrl, {
9032
+ method: String(req.method ?? "POST").toUpperCase(),
9033
+ headers: buildRemoteForwardHeaders(req, remoteService.auth_token),
9034
+ body: req.body === void 0 ? void 0 : JSON.stringify(req.body),
9035
+ signal: AbortSignal.timeout(config.API_TIMEOUT_MS ?? 6e5)
9036
+ });
9037
+ reply.code(response.status);
9038
+ const contentType = response.headers.get("content-type");
9039
+ if (contentType) {
9040
+ reply.header?.("content-type", contentType);
9041
+ }
9042
+ const retryAfter = response.headers.get("retry-after");
9043
+ if (retryAfter) {
9044
+ reply.header?.("retry-after", retryAfter);
9045
+ }
9046
+ req.remoteForwarded = true;
9047
+ req.responseGovernanceApplied = true;
9048
+ if (response.body) {
9049
+ reply.send(response.body);
9050
+ return true;
9051
+ }
9052
+ reply.send(response.ok ? {} : { error: `Remote service returned HTTP ${response.status}` });
9053
+ return true;
9054
+ } catch (error) {
9055
+ req.remoteForwarded = true;
9056
+ req.responseGovernanceApplied = true;
9057
+ logWarn(`[RemoteForward] Failed to forward ${path}: ${error instanceof Error ? error.message : String(error)}`);
9058
+ reply.code(502);
9059
+ reply.send({
9060
+ error: {
9061
+ type: "remote_service_unavailable",
9062
+ message: "Remote CTR service is unavailable.",
9063
+ remoteService: remoteBaseUrl
9064
+ }
9065
+ });
9066
+ return true;
9067
+ }
9068
+ }
7834
9069
  async function run(options = {}) {
7835
9070
  if (isServiceRunning()) {
7836
9071
  log("\u2705 Service is already running in the background.");
@@ -7844,6 +9079,17 @@ async function run(options = {}) {
7844
9079
  } catch (error) {
7845
9080
  logWarn(`[AuthQuota] Failed to load persisted quota usage: ${error instanceof Error ? error.message : String(error)}`);
7846
9081
  }
9082
+ try {
9083
+ modelPoolHealthStore.hydrate(await loadPersistedModelPoolHealth());
9084
+ } catch (error) {
9085
+ logWarn(`[ModelPoolHealth] Failed to load persisted health state: ${error instanceof Error ? error.message : String(error)}`);
9086
+ }
9087
+ const modelPoolHealthPersistence = createModelPoolHealthPersistenceScheduler({
9088
+ onError: (error) => {
9089
+ logWarn(`[ModelPoolHealth] Failed to persist health state: ${error instanceof Error ? error.message : String(error)}`);
9090
+ }
9091
+ });
9092
+ modelPoolHealthStore.setChangeListener(modelPoolHealthPersistence.schedule);
7847
9093
  configureLogging(config);
7848
9094
  let HOST = config.HOST || "127.0.0.1";
7849
9095
  const managedKeySummary = managedApiKeySummary(config);
@@ -7854,15 +9100,21 @@ async function run(options = {}) {
7854
9100
  }
7855
9101
  const port = options.port ?? config.PORT ?? DEFAULT_CONFIG.PORT;
7856
9102
  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", () => {
9103
+ const shutdown = (signal) => {
9104
+ log(`Received ${signal}, cleaning up...`);
7863
9105
  cleanupPidFile();
7864
- process.exit(0);
7865
- });
9106
+ const forceExit = setTimeout(() => process.exit(0), 500);
9107
+ forceExit.unref?.();
9108
+ void Promise.allSettled([
9109
+ governanceTraceStore.flushPersistence(),
9110
+ modelPoolHealthPersistence.flush(modelPoolHealthStore.exportForPersistence())
9111
+ ]).finally(() => {
9112
+ clearTimeout(forceExit);
9113
+ process.exit(0);
9114
+ });
9115
+ };
9116
+ process.on("SIGINT", () => shutdown("SIGINT"));
9117
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
7866
9118
  const servicePort = process.env.SERVICE_PORT ? parseInt(process.env.SERVICE_PORT) : port;
7867
9119
  config.PORT = servicePort;
7868
9120
  const pad = (num) => (num > 9 ? "" : "0") + num;
@@ -7893,19 +9145,37 @@ async function run(options = {}) {
7893
9145
  initialConfig: buildServerInitialConfig(config, registry, HOST, servicePort),
7894
9146
  logger: loggerConfig
7895
9147
  });
7896
- const authMiddleware = apiKeyAuth(async () => {
7897
- try {
7898
- const currentConfig = await readConfigFile();
7899
- return {
9148
+ const authConfigTtlMs = 1e3;
9149
+ let cachedAuthConfig;
9150
+ let cachedAuthConfigExpiresAt = 0;
9151
+ let pendingAuthConfigRefresh;
9152
+ const getAuthConfig = async () => {
9153
+ const now = Date.now();
9154
+ if (cachedAuthConfig && now < cachedAuthConfigExpiresAt) {
9155
+ return cachedAuthConfig;
9156
+ }
9157
+ if (pendingAuthConfigRefresh) {
9158
+ return pendingAuthConfigRefresh;
9159
+ }
9160
+ pendingAuthConfigRefresh = readConfigFile().then((currentConfig) => {
9161
+ cachedAuthConfig = {
7900
9162
  ...config,
7901
9163
  APIKEY: currentConfig.APIKEY,
7902
9164
  Auth: currentConfig.Auth
7903
9165
  };
7904
- } catch (error) {
9166
+ cachedAuthConfigExpiresAt = Date.now() + authConfigTtlMs;
9167
+ return cachedAuthConfig;
9168
+ }).catch((error) => {
7905
9169
  logWarn(`[Auth] Failed to refresh auth config, using startup auth config: ${error instanceof Error ? error.message : String(error)}`);
9170
+ cachedAuthConfig = config;
9171
+ cachedAuthConfigExpiresAt = Date.now() + authConfigTtlMs;
7906
9172
  return config;
7907
- }
7908
- }, {
9173
+ }).finally(() => {
9174
+ pendingAuthConfigRefresh = void 0;
9175
+ });
9176
+ return pendingAuthConfigRefresh;
9177
+ };
9178
+ const authMiddleware = apiKeyAuth(getAuthConfig, {
7909
9179
  persistQuotaUsage: async (usage) => {
7910
9180
  try {
7911
9181
  await savePersistedAuthQuotaUsage(usage);
@@ -7923,9 +9193,17 @@ async function run(options = {}) {
7923
9193
  authMiddleware(req, reply, done);
7924
9194
  });
7925
9195
  });
9196
+ server.addHook("preHandler", async (req, reply) => {
9197
+ if (await forwardModelCallToRemote(req, reply, config)) {
9198
+ return reply;
9199
+ }
9200
+ });
7926
9201
  triggerRouter.init(config);
7927
9202
  log(`[SmartRouter] Initialized, enabled: ${triggerRouter.isEnabled()}`);
7928
9203
  server.addHook("preHandler", async (req, reply) => {
9204
+ if (req.remoteForwarded) {
9205
+ return;
9206
+ }
7929
9207
  if (req.url.startsWith("/v1/messages")) {
7930
9208
  if (req.body.metadata?.user_id) {
7931
9209
  const parts = req.body.metadata.user_id.split("_session_");
@@ -8004,6 +9282,24 @@ async function run(options = {}) {
8004
9282
  config,
8005
9283
  event
8006
9284
  });
9285
+ if (req.contextWindowExceeded) {
9286
+ req.responseGovernanceApplied = true;
9287
+ req.localStructuredError = true;
9288
+ if (req.governanceTrace) {
9289
+ req.governanceTrace = finalizeTrace(req.governanceTrace, {
9290
+ finalModel: req.body?.model ?? req.governanceTrace.finalModel
9291
+ });
9292
+ recordGovernanceTrace(req.governanceTrace);
9293
+ }
9294
+ reply.code(413);
9295
+ return reply.send({
9296
+ error: {
9297
+ type: "context_window_exceeded",
9298
+ message: "Selected model cannot safely handle the current request context.",
9299
+ details: req.contextWindowExceeded
9300
+ }
9301
+ });
9302
+ }
8007
9303
  const compiledModel = getCompiledModelRef(config, req.body?.model);
8008
9304
  if (compiledModel?.interface && req.body?.messages) {
8009
9305
  const originalBody = cloneRequestBody(req.body);
@@ -8035,6 +9331,9 @@ async function run(options = {}) {
8035
9331
  event.emit("onError", request, reply, error);
8036
9332
  });
8037
9333
  server.addHook("onSend", (req, reply, payload, done) => {
9334
+ if (req.remoteForwarded) {
9335
+ return done(null, payload);
9336
+ }
8038
9337
  if (req.originalRequestBody) {
8039
9338
  req.body = req.originalRequestBody;
8040
9339
  }
@@ -8176,6 +9475,27 @@ async function run(options = {}) {
8176
9475
  sessionUsageCache.put(req.sessionId, payload.usage);
8177
9476
  }
8178
9477
  if (typeof payload === "object" && payload.error) {
9478
+ if (req.localStructuredError || payload.error?.type === "context_window_exceeded") {
9479
+ return done(null, payload);
9480
+ }
9481
+ if (req.modelPoolSelection) {
9482
+ applyResponseGovernance({
9483
+ req,
9484
+ payload,
9485
+ config,
9486
+ servicePort
9487
+ }).then((governedPayload) => {
9488
+ req.responseGovernanceApplied = true;
9489
+ if (governedPayload && typeof governedPayload === "object" && governedPayload.error) {
9490
+ return done(governedPayload.error, null);
9491
+ }
9492
+ if (req.sessionId && governedPayload?.usage) {
9493
+ sessionUsageCache.put(req.sessionId, governedPayload.usage);
9494
+ }
9495
+ return done(null, governedPayload);
9496
+ }).catch((error) => done(error, null));
9497
+ return;
9498
+ }
8179
9499
  return done(payload.error, null);
8180
9500
  }
8181
9501
  done(null, payload);
@@ -8184,12 +9504,14 @@ async function run(options = {}) {
8184
9504
  if (payload instanceof ReadableStream) {
8185
9505
  return payload;
8186
9506
  }
8187
- payload = await applyResponseGovernance({
8188
- req,
8189
- payload,
8190
- config,
8191
- servicePort
8192
- });
9507
+ if (!req.responseGovernanceApplied) {
9508
+ payload = await applyResponseGovernance({
9509
+ req,
9510
+ payload,
9511
+ config,
9512
+ servicePort
9513
+ });
9514
+ }
8193
9515
  if (req.governanceTrace) {
8194
9516
  logDebug("[GovernanceTrace]", JSON.stringify(req.governanceTrace));
8195
9517
  }
@@ -8199,14 +9521,14 @@ async function run(options = {}) {
8199
9521
  });
8200
9522
  await server.start();
8201
9523
  }
8202
- var import_fs6, import_promises3, import_os2, import_path6, import_json5, import_node_events, import_rotating_file_stream, event;
9524
+ var import_fs7, import_promises5, import_os2, import_path7, import_json5, import_node_events, import_rotating_file_stream, event;
8203
9525
  var init_index = __esm({
8204
9526
  "src/index.ts"() {
8205
9527
  "use strict";
8206
- import_fs6 = require("fs");
8207
- import_promises3 = require("fs/promises");
9528
+ import_fs7 = require("fs");
9529
+ import_promises5 = require("fs/promises");
8208
9530
  import_os2 = require("os");
8209
- import_path6 = require("path");
9531
+ import_path7 = require("path");
8210
9532
  init_utils();
8211
9533
  init_server();
8212
9534
  init_router();
@@ -8227,6 +9549,8 @@ var init_index = __esm({
8227
9549
  import_rotating_file_stream = require("rotating-file-stream");
8228
9550
  init_governance();
8229
9551
  init_compile();
9552
+ init_pool_health();
9553
+ init_pool_health_persistence();
8230
9554
  init_protocols();
8231
9555
  event = new import_node_events.EventEmitter();
8232
9556
  }
@@ -9053,7 +10377,7 @@ var init_setup = __esm({
9053
10377
  // src/setup/index.ts
9054
10378
  function createConsoleIO() {
9055
10379
  if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
9056
- const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
10380
+ const scriptedInput = (0, import_fs8.readFileSync)(0, "utf-8");
9057
10381
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
9058
10382
  let cursor = 0;
9059
10383
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -9142,7 +10466,7 @@ function createConsoleIO() {
9142
10466
  }
9143
10467
  };
9144
10468
  }
9145
- const rl = (0, import_promises4.createInterface)({ input: import_process.stdin, output: import_process.stdout });
10469
+ const rl = (0, import_promises6.createInterface)({ input: import_process.stdin, output: import_process.stdout });
9146
10470
  const ask = async (message) => {
9147
10471
  const answer = await rl.question(message);
9148
10472
  return answer.trim();
@@ -9183,7 +10507,7 @@ function createConsoleIO() {
9183
10507
  };
9184
10508
  }
9185
10509
  function readStructuredConfigFile(filePath) {
9186
- const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
10510
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
9187
10511
  if (filePath.endsWith(".json")) {
9188
10512
  return JSON.parse(content);
9189
10513
  }
@@ -9191,7 +10515,7 @@ function readStructuredConfigFile(filePath) {
9191
10515
  }
9192
10516
  function getCurrentRuntimeFields() {
9193
10517
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
9194
- const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
10518
+ const currentPath = candidates.find((filePath) => (0, import_fs8.existsSync)(filePath));
9195
10519
  if (!currentPath) {
9196
10520
  return {};
9197
10521
  }
@@ -9213,7 +10537,7 @@ function getCurrentRuntimeFields() {
9213
10537
  }
9214
10538
  function getConfiguredPortFromCurrentFiles() {
9215
10539
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
9216
- const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
10540
+ const currentPath = candidates.find((filePath) => (0, import_fs8.existsSync)(filePath));
9217
10541
  if (!currentPath) {
9218
10542
  return DEFAULT_CONFIG2.PORT;
9219
10543
  }
@@ -9247,7 +10571,7 @@ async function getAvailablePort() {
9247
10571
  }
9248
10572
  }
9249
10573
  function readLegacyConfigFile(filePath) {
9250
- const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
10574
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
9251
10575
  if (filePath.endsWith(".json")) {
9252
10576
  return import_json52.default.parse(content);
9253
10577
  }
@@ -9255,13 +10579,13 @@ function readLegacyConfigFile(filePath) {
9255
10579
  }
9256
10580
  async function readLegacyConfig(deps = {}) {
9257
10581
  const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
9258
- const exists = deps.exists || import_fs7.existsSync;
10582
+ const exists = deps.exists || import_fs8.existsSync;
9259
10583
  const readConfig = deps.readConfig || readLegacyConfigFile;
9260
10584
  const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
9261
10585
  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")
10586
+ (0, import_path8.join)(baseHomeDir, ".ccr", "config.yaml"),
10587
+ (0, import_path8.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
10588
+ (0, import_path8.join)(baseHomeDir, ".claude-code-router", "config.json")
9265
10589
  ];
9266
10590
  const legacyPath = candidatePaths.find((filePath) => exists(filePath));
9267
10591
  if (!legacyPath) {
@@ -9283,7 +10607,7 @@ async function readLegacyConfig(deps = {}) {
9283
10607
  }
9284
10608
  async function readCurrentConfig() {
9285
10609
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
9286
- const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
10610
+ const currentPath = candidates.find((filePath) => (0, import_fs8.existsSync)(filePath));
9287
10611
  if (!currentPath) {
9288
10612
  return { kind: "missing" };
9289
10613
  }
@@ -9967,16 +11291,16 @@ async function runSetupCli(customDeps) {
9967
11291
  deps.io.close?.();
9968
11292
  }
9969
11293
  }
9970
- var import_fs7, import_crypto3, import_net2, import_os3, import_path7, import_promises4, import_process, import_json52, import_js_yaml;
11294
+ var import_fs8, import_crypto3, import_net2, import_os3, import_path8, import_promises6, import_process, import_json52, import_js_yaml;
9971
11295
  var init_setup2 = __esm({
9972
11296
  "src/setup/index.ts"() {
9973
11297
  "use strict";
9974
- import_fs7 = require("fs");
11298
+ import_fs8 = require("fs");
9975
11299
  import_crypto3 = require("crypto");
9976
11300
  import_net2 = require("net");
9977
11301
  import_os3 = require("os");
9978
- import_path7 = require("path");
9979
- import_promises4 = require("readline/promises");
11302
+ import_path8 = require("path");
11303
+ import_promises6 = require("readline/promises");
9980
11304
  import_process = require("process");
9981
11305
  import_json52 = __toESM(require("json5"));
9982
11306
  import_js_yaml = __toESM(require("js-yaml"));
@@ -10067,7 +11391,7 @@ function hasArg(flag) {
10067
11391
  }
10068
11392
  function createConsoleIO2() {
10069
11393
  if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
10070
- const scriptedInput = (0, import_fs8.readFileSync)(0, "utf-8");
11394
+ const scriptedInput = (0, import_fs9.readFileSync)(0, "utf-8");
10071
11395
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
10072
11396
  let cursor = 0;
10073
11397
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -10110,7 +11434,7 @@ function createConsoleIO2() {
10110
11434
  }
10111
11435
  };
10112
11436
  }
10113
- const rl = (0, import_promises5.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
11437
+ const rl = (0, import_promises7.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
10114
11438
  const ask = async (message) => {
10115
11439
  try {
10116
11440
  return (await rl.question(message)).trim();
@@ -10203,7 +11527,7 @@ function tryLoadStructuredConfig(filePath, content) {
10203
11527
  }
10204
11528
  }
10205
11529
  function loadCurrentConfig() {
10206
- const existingPath = getConfigCandidates().find((filePath) => (0, import_fs8.existsSync)(filePath));
11530
+ const existingPath = getConfigCandidates().find((filePath) => (0, import_fs9.existsSync)(filePath));
10207
11531
  const path = existingPath ?? CONFIG_FILE;
10208
11532
  if (!existingPath) {
10209
11533
  return {
@@ -10213,7 +11537,7 @@ function loadCurrentConfig() {
10213
11537
  messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
10214
11538
  };
10215
11539
  }
10216
- const content = (0, import_fs8.readFileSync)(existingPath, "utf-8");
11540
+ const content = (0, import_fs9.readFileSync)(existingPath, "utf-8");
10217
11541
  const loaded = tryLoadStructuredConfig(existingPath, content);
10218
11542
  return {
10219
11543
  path,
@@ -10532,14 +11856,14 @@ async function reportRuntimeServiceContext(config, deps) {
10532
11856
  deps.io.info(`\u670D\u52A1\u4E0A\u4E0B\u6587\uFF1A${runtimeMode}\uFF08${serviceRole}\uFF09`);
10533
11857
  deps.io.info(`\u76D1\u542C\u5730\u5740\uFF1A${host}:${port}${publicHost ? "\uFF08\u5BF9\u5916\u76D1\u542C\uFF09" : "\uFF08\u672C\u673A\u76D1\u542C\uFF09"}`);
10534
11858
  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");
11859
+ 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
11860
  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
11861
  if (runtimeMode !== "local") {
10538
11862
  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
11863
  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
11864
  }
10541
11865
  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");
11866
+ 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
11867
  } else if (!hasBootstrapAuth && hasManagedAuthRecords && managedKeys.active === 0) {
10544
11868
  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
11869
  } else if (authRequired && hasBootstrapAuth && managedKeys.total === 0 && runtimeMode !== "local") {
@@ -10567,16 +11891,99 @@ async function reportRuntimeServiceContext(config, deps) {
10567
11891
  deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u63D0\u793A\uFF1A${remoteStatus.error}`);
10568
11892
  }
10569
11893
  }
10570
- function createDefaultDeps2(io = createConsoleIO2()) {
10571
- return {
10572
- readLegacyConfig,
10573
- backupCurrentConfig: backupConfigFile,
10574
- writeConfig: writeConfigFile,
10575
- isServiceRunning,
10576
- readServiceInfo,
10577
- killProcess,
10578
- probeServiceHealth,
10579
- isTcpPortOccupied,
11894
+ function getRouterSlotRef(config, key) {
11895
+ const value = config.Router?.[key];
11896
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
11897
+ }
11898
+ function getCompiledModelFromRegistry(registry, ref) {
11899
+ if (!ref) {
11900
+ return void 0;
11901
+ }
11902
+ const direct = registry.modelMap[ref];
11903
+ if (direct) {
11904
+ return direct;
11905
+ }
11906
+ if (!ref.includes(",")) {
11907
+ return void 0;
11908
+ }
11909
+ const [providerName, modelName] = ref.split(",").map((item) => item.trim());
11910
+ return Object.values(registry.modelMap).find(
11911
+ (item) => item.providerName === providerName && item.modelName === modelName
11912
+ );
11913
+ }
11914
+ function formatRouterSlotTarget(compiled) {
11915
+ const upstream = `${compiled.providerName},${compiled.modelName}`;
11916
+ return compiled.id === upstream ? compiled.id : `${compiled.id}\uFF08${upstream}\uFF09`;
11917
+ }
11918
+ function reportRouterSlotSummary(config, registry, deps) {
11919
+ const modelRefCount = Object.keys(registry.modelMap).length;
11920
+ const resolvedSlots = /* @__PURE__ */ new Map();
11921
+ deps.io.info("\u57FA\u7840\u8DEF\u7531\u4F53\u68C0\uFF1A\u68C0\u67E5 Router \u69FD\u4F4D\u662F\u5426\u80FD\u89E3\u6790\u4E3A\u53EF\u7528\u6A21\u578B\u3002");
11922
+ for (const slot of ROUTER_SLOT_DIAGNOSTICS) {
11923
+ const ref = getRouterSlotRef(config, slot.key);
11924
+ if (!ref) {
11925
+ const message = `\u8DEF\u7531\u69FD\u4F4D\uFF1ARouter.${slot.key} \u672A\u914D\u7F6E\uFF08${slot.fallback}\uFF09\u3002`;
11926
+ if (slot.required && modelRefCount > 0) {
11927
+ deps.io.error(message);
11928
+ } else {
11929
+ deps.io.info(message);
11930
+ }
11931
+ continue;
11932
+ }
11933
+ const compiled = getCompiledModelFromRegistry(registry, ref);
11934
+ if (!compiled) {
11935
+ deps.io.error(`\u8DEF\u7531\u69FD\u4F4D\u5F02\u5E38\uFF1ARouter.${slot.key} \u5F15\u7528 "${ref}"\uFF0C\u4F46\u672A\u5728 Models/Providers/Registration \u4E2D\u89E3\u6790\u5230\u53EF\u7528\u6A21\u578B\u3002`);
11936
+ continue;
11937
+ }
11938
+ resolvedSlots.set(slot.key, compiled);
11939
+ deps.io.info(`\u8DEF\u7531\u69FD\u4F4D\uFF1ARouter.${slot.key}\uFF08${slot.label}\uFF09-> ${formatRouterSlotTarget(compiled)}\uFF1B\u89E6\u53D1\uFF1A${slot.trigger}\u3002`);
11940
+ }
11941
+ const thinkingSlot = resolvedSlots.get("think");
11942
+ if (thinkingSlot && thinkingSlot.capabilities.thinking.supported === false) {
11943
+ deps.io.info(
11944
+ `\u601D\u8003\u8DEF\u7531\u63D0\u793A\uFF1ARouter.think \u6307\u5411 ${thinkingSlot.id}\uFF0C\u4F46\u8BE5\u6A21\u578B\u58F0\u660E\u4E0D\u652F\u6301 reasoning\uFF1Bthinking \u8BF7\u6C42\u4F1A\u88AB\u517C\u5BB9\u5C42\u964D\u7EA7\u3002`
11945
+ );
11946
+ }
11947
+ for (const [slotKey, compiled] of resolvedSlots.entries()) {
11948
+ const contextWindowTokens = compiled.capabilities.contextWindowTokens;
11949
+ const safeInputTokens = compiled.capabilities.safeInputTokens;
11950
+ if (!contextWindowTokens) {
11951
+ deps.io.info(
11952
+ `\u4E0A\u4E0B\u6587\u7A97\u53E3\u63D0\u793A\uFF1ARouter.${slotKey} -> ${compiled.id} \u672A\u58F0\u660E metadata.context_window_tokens\uFF1B\u65E0\u6CD5\u786E\u8BA4\u8BE5\u69FD\u4F4D\u7684\u4E0A\u4E0B\u6587\u5BB9\u91CF\u3002`
11953
+ );
11954
+ }
11955
+ if (!safeInputTokens) {
11956
+ deps.io.info(
11957
+ `\u4E0A\u4E0B\u6587\u4FDD\u62A4\u63D0\u793A\uFF1ARouter.${slotKey} -> ${compiled.id} \u672A\u58F0\u660E metadata.safe_input_tokens\uFF1B\u65E0\u6CD5\u63D0\u524D\u628A\u8D85\u5927\u8BF7\u6C42\u5207\u5230\u957F\u4E0A\u4E0B\u6587\u6A21\u578B\u3002`
11958
+ );
11959
+ }
11960
+ }
11961
+ const defaultSlot = resolvedSlots.get("default");
11962
+ const longContextSlot = resolvedSlots.get("longContext");
11963
+ if (!longContextSlot && modelRefCount > 1) {
11964
+ deps.io.info("\u957F\u4E0A\u4E0B\u6587\u63D0\u793A\uFF1A\u672A\u914D\u7F6E Router.longContext\uFF1B\u591A\u6A21\u578B\u914D\u7F6E\u4E0B\uFF0C\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u4E0D\u4F1A\u81EA\u52A8\u5207\u5230\u66F4\u5927\u7A97\u53E3\u6A21\u578B\u3002");
11965
+ return;
11966
+ }
11967
+ if (!longContextSlot) {
11968
+ return;
11969
+ }
11970
+ if (!longContextSlot.capabilities.contextWindowTokens) {
11971
+ deps.io.info("\u957F\u4E0A\u4E0B\u6587\u63D0\u793A\uFF1ARouter.longContext \u672A\u58F0\u660E metadata.context_window_tokens\uFF1Bdoctor \u65E0\u6CD5\u786E\u8BA4\u5B83\u80FD\u627F\u63A5\u5927\u4E0A\u4E0B\u6587 fallback\u3002");
11972
+ }
11973
+ if (defaultSlot?.capabilities.contextWindowTokens && longContextSlot.capabilities.contextWindowTokens && longContextSlot.capabilities.contextWindowTokens <= defaultSlot.capabilities.contextWindowTokens) {
11974
+ deps.io.info("\u957F\u4E0A\u4E0B\u6587\u63D0\u793A\uFF1ARouter.longContext \u7684 context_window_tokens \u4E0D\u9AD8\u4E8E Router.default\uFF1B\u8BF7\u786E\u8BA4\u5B83\u786E\u5B9E\u662F\u957F\u4E0A\u4E0B\u6587\u6A21\u578B\u3002");
11975
+ }
11976
+ }
11977
+ function createDefaultDeps2(io = createConsoleIO2()) {
11978
+ return {
11979
+ readLegacyConfig,
11980
+ backupCurrentConfig: backupConfigFile,
11981
+ writeConfig: writeConfigFile,
11982
+ isServiceRunning,
11983
+ readServiceInfo,
11984
+ killProcess,
11985
+ probeServiceHealth,
11986
+ isTcpPortOccupied,
10580
11987
  waitForService,
10581
11988
  io,
10582
11989
  startDaemon: async () => {
@@ -10626,6 +12033,7 @@ async function runDoctorCli(customDeps) {
10626
12033
  }
10627
12034
  await reportRuntimeServiceContext(normalized.config, deps);
10628
12035
  const registry = buildModelRegistry(normalized.config);
12036
+ reportRouterSlotSummary(normalized.config, registry, deps);
10629
12037
  for (const model of normalized.config.Models ?? []) {
10630
12038
  const compiledModel = registry.modelMap[model.id];
10631
12039
  if (!compiledModel) {
@@ -10691,12 +12099,12 @@ async function runDoctorCli(customDeps) {
10691
12099
  deps.io.close?.();
10692
12100
  }
10693
12101
  }
10694
- var import_fs8, import_promises5, import_process2, import_child_process2, import_json53, import_js_yaml2;
12102
+ var import_fs9, import_promises7, import_process2, import_child_process2, import_json53, import_js_yaml2, ROUTER_SLOT_DIAGNOSTICS;
10695
12103
  var init_doctor = __esm({
10696
12104
  "src/doctor/index.ts"() {
10697
12105
  "use strict";
10698
- import_fs8 = require("fs");
10699
- import_promises5 = require("readline/promises");
12106
+ import_fs9 = require("fs");
12107
+ import_promises7 = require("readline/promises");
10700
12108
  import_process2 = require("process");
10701
12109
  import_child_process2 = require("child_process");
10702
12110
  import_json53 = __toESM(require("json5"));
@@ -10713,6 +12121,912 @@ var init_doctor = __esm({
10713
12121
  init_service_health();
10714
12122
  init_templates();
10715
12123
  init_api_keys();
12124
+ ROUTER_SLOT_DIAGNOSTICS = [
12125
+ {
12126
+ key: "default",
12127
+ label: "\u9ED8\u8BA4",
12128
+ required: true,
12129
+ fallback: "\u65E0\u9ED8\u8BA4\u6A21\u578B\u65F6\u672C\u5730\u670D\u52A1\u65E0\u6CD5\u7A33\u5B9A\u627F\u63A5\u8BF7\u6C42",
12130
+ trigger: "\u666E\u901A\u8BF7\u6C42\u548C\u5176\u4ED6\u69FD\u4F4D\u672A\u547D\u4E2D\u65F6\u4F7F\u7528"
12131
+ },
12132
+ {
12133
+ key: "think",
12134
+ label: "\u601D\u8003",
12135
+ required: false,
12136
+ fallback: "\u672A\u914D\u7F6E\u65F6 thinking \u8BF7\u6C42\u56DE\u5230 Router.default",
12137
+ trigger: "\u8BF7\u6C42\u5305\u542B thinking \u65F6\u4F7F\u7528"
12138
+ },
12139
+ {
12140
+ key: "longContext",
12141
+ label: "\u957F\u4E0A\u4E0B\u6587",
12142
+ required: false,
12143
+ fallback: "\u672A\u914D\u7F6E\u65F6\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u7EE7\u7EED\u4F7F\u7528\u5DF2\u9009\u6A21\u578B",
12144
+ trigger: "\u8BF7\u6C42 token \u8D85\u8FC7 longContextThreshold \u6216\u5F53\u524D\u6A21\u578B\u5B89\u5168\u8F93\u5165\u7A97\u53E3\u65F6\u4F7F\u7528"
12145
+ },
12146
+ {
12147
+ key: "background",
12148
+ label: "\u540E\u53F0",
12149
+ required: false,
12150
+ fallback: "\u672A\u914D\u7F6E\u65F6\u540E\u53F0/\u8F7B\u91CF\u8BF7\u6C42\u56DE\u5230 Router.default",
12151
+ trigger: "Claude Code \u8F7B\u91CF\u540E\u53F0\u6A21\u578B\u8BF7\u6C42\u65F6\u4F7F\u7528"
12152
+ },
12153
+ {
12154
+ key: "webSearch",
12155
+ label: "\u8054\u7F51\u641C\u7D22",
12156
+ required: false,
12157
+ fallback: "\u672A\u914D\u7F6E\u65F6 web_search \u8BF7\u6C42\u56DE\u5230 Router.default",
12158
+ trigger: "\u8BF7\u6C42\u5305\u542B web_search \u5DE5\u5177\u65F6\u4F7F\u7528"
12159
+ }
12160
+ ];
12161
+ }
12162
+ });
12163
+
12164
+ // src/governance/task-evaluation.ts
12165
+ function parseOfflineEvaluationInputs(payload) {
12166
+ const rawResults = Array.isArray(payload) ? payload : typeof payload === "object" && payload !== null && Array.isArray(payload.results) ? payload.results : void 0;
12167
+ if (!rawResults) {
12168
+ 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");
12169
+ }
12170
+ return rawResults.map((item, index) => {
12171
+ if (typeof item !== "object" || item === null) {
12172
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u5FC5\u987B\u662F\u5BF9\u8C61\u3002`);
12173
+ }
12174
+ const record = item;
12175
+ if (typeof record.taskId !== "string" || !record.taskId.trim()) {
12176
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7F3A\u5C11 taskId\u3002`);
12177
+ }
12178
+ if (typeof record.model !== "string" || !record.model.trim()) {
12179
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7F3A\u5C11 model\u3002`);
12180
+ }
12181
+ if (record.output !== void 0 && typeof record.output !== "string") {
12182
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 output \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
12183
+ }
12184
+ if (record.error !== void 0 && typeof record.error !== "string") {
12185
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 error \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
12186
+ }
12187
+ if (record.judgeError !== void 0 && record.judgeError !== null && typeof record.judgeError !== "string") {
12188
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 judgeError \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
12189
+ }
12190
+ if (record.latencyMs !== void 0 && (typeof record.latencyMs !== "number" || !Number.isFinite(record.latencyMs) || record.latencyMs < 0)) {
12191
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 latencyMs \u5FC5\u987B\u662F\u975E\u8D1F\u6570\u5B57\u3002`);
12192
+ }
12193
+ const humanScore = parseOptionalUnitScore(record.humanScore, `\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 humanScore`);
12194
+ const judgeScore = parseOptionalUnitScore(record.judgeScore, `\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 judgeScore`);
12195
+ if (record.calibrationNotes !== void 0 && record.calibrationNotes !== null && typeof record.calibrationNotes !== "string") {
12196
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 calibrationNotes \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
12197
+ }
12198
+ if (record.judgeFindings !== void 0 && record.judgeFindings !== null && (!Array.isArray(record.judgeFindings) || record.judgeFindings.some((item2) => typeof item2 !== "string"))) {
12199
+ throw new Error(`\u7B2C ${index + 1} \u6761\u8BC4\u6D4B\u7ED3\u679C\u7684 judgeFindings \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\u3002`);
12200
+ }
12201
+ return {
12202
+ taskId: record.taskId.trim(),
12203
+ model: record.model.trim(),
12204
+ output: record.output,
12205
+ error: record.error,
12206
+ latencyMs: record.latencyMs,
12207
+ humanScore,
12208
+ judgeScore,
12209
+ judgeError: typeof record.judgeError === "string" ? record.judgeError : void 0,
12210
+ calibrationNotes: typeof record.calibrationNotes === "string" ? record.calibrationNotes : void 0,
12211
+ judgeFindings: Array.isArray(record.judgeFindings) ? record.judgeFindings : void 0
12212
+ };
12213
+ });
12214
+ }
12215
+ function clamp(value) {
12216
+ return Math.max(0, Math.min(1, Number(value.toFixed(4))));
12217
+ }
12218
+ function average2(values) {
12219
+ if (!values.length) {
12220
+ return 0;
12221
+ }
12222
+ return Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(4));
12223
+ }
12224
+ function parseOptionalUnitScore(value, label) {
12225
+ if (value === void 0 || value === null) {
12226
+ return void 0;
12227
+ }
12228
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
12229
+ throw new Error(`${label} \u5FC5\u987B\u662F 0 \u5230 1 \u4E4B\u95F4\u7684\u6570\u5B57\u3002`);
12230
+ }
12231
+ return Number(value.toFixed(4));
12232
+ }
12233
+ function rate2(count, total) {
12234
+ if (!total) {
12235
+ return 0;
12236
+ }
12237
+ return Number((count / total).toFixed(4));
12238
+ }
12239
+ function normalizeText(value) {
12240
+ return value.toLowerCase();
12241
+ }
12242
+ function includesCodeBlock(output3) {
12243
+ return /```[\s\S]*```/.test(output3);
12244
+ }
12245
+ function normalizeDimensionId(value) {
12246
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "quality";
12247
+ }
12248
+ function qualityDimensionsForTask(task) {
12249
+ if (Array.isArray(task.qualityDimensions) && task.qualityDimensions.length) {
12250
+ return task.qualityDimensions.map((dimension) => ({
12251
+ ...dimension,
12252
+ id: normalizeDimensionId(dimension.id)
12253
+ }));
12254
+ }
12255
+ const dimensions = [];
12256
+ if ((task.requiredKeywords ?? []).length) {
12257
+ dimensions.push({
12258
+ id: "semantic_coverage",
12259
+ label: "Semantic coverage",
12260
+ weight: 0.45,
12261
+ minScore: 0.7,
12262
+ requiredKeywords: task.requiredKeywords
12263
+ });
12264
+ }
12265
+ if ((task.minOutputChars ?? 0) > 0) {
12266
+ dimensions.push({
12267
+ id: "completeness",
12268
+ label: "Completeness",
12269
+ weight: 0.25,
12270
+ minScore: 0.7,
12271
+ minOutputChars: task.minOutputChars
12272
+ });
12273
+ }
12274
+ if (task.requiresCodeBlock) {
12275
+ dimensions.push({
12276
+ id: "deliverable_format",
12277
+ label: "Deliverable format",
12278
+ weight: 0.2,
12279
+ minScore: 1,
12280
+ requiresCodeBlock: true
12281
+ });
12282
+ }
12283
+ if ((task.forbiddenPatterns ?? []).length) {
12284
+ dimensions.push({
12285
+ id: "safety_hygiene",
12286
+ label: "Safety and hygiene",
12287
+ weight: 0.1,
12288
+ minScore: 1,
12289
+ forbiddenPatterns: task.forbiddenPatterns
12290
+ });
12291
+ }
12292
+ return dimensions;
12293
+ }
12294
+ function extractResponseText(payload) {
12295
+ if (!payload) {
12296
+ return "";
12297
+ }
12298
+ if (typeof payload === "string") {
12299
+ return payload;
12300
+ }
12301
+ if (typeof payload.output_text === "string") {
12302
+ return payload.output_text;
12303
+ }
12304
+ if (typeof payload.content === "string") {
12305
+ return payload.content;
12306
+ }
12307
+ if (Array.isArray(payload.content)) {
12308
+ return extractContentText(payload.content);
12309
+ }
12310
+ if (Array.isArray(payload.choices)) {
12311
+ return payload.choices.map((choice) => extractContentText(choice?.message?.content ?? choice?.text ?? "")).filter(Boolean).join("\n");
12312
+ }
12313
+ return "";
12314
+ }
12315
+ function extractContentText(content) {
12316
+ if (typeof content === "string") {
12317
+ return content;
12318
+ }
12319
+ if (!Array.isArray(content)) {
12320
+ return "";
12321
+ }
12322
+ return content.map((part) => {
12323
+ if (typeof part === "string") {
12324
+ return part;
12325
+ }
12326
+ if (typeof part?.text === "string") {
12327
+ return part.text;
12328
+ }
12329
+ return "";
12330
+ }).filter(Boolean).join("\n");
12331
+ }
12332
+ function extractFirstJsonObject(text) {
12333
+ const start = text.indexOf("{");
12334
+ if (start === -1) {
12335
+ return void 0;
12336
+ }
12337
+ let depth = 0;
12338
+ let inString = false;
12339
+ let escaped = false;
12340
+ for (let index = start; index < text.length; index += 1) {
12341
+ const char = text[index];
12342
+ if (escaped) {
12343
+ escaped = false;
12344
+ continue;
12345
+ }
12346
+ if (char === "\\") {
12347
+ escaped = true;
12348
+ continue;
12349
+ }
12350
+ if (char === '"') {
12351
+ inString = !inString;
12352
+ continue;
12353
+ }
12354
+ if (inString) {
12355
+ continue;
12356
+ }
12357
+ if (char === "{") {
12358
+ depth += 1;
12359
+ } else if (char === "}") {
12360
+ depth -= 1;
12361
+ if (depth === 0) {
12362
+ try {
12363
+ const payload = JSON.parse(text.slice(start, index + 1));
12364
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : void 0;
12365
+ } catch {
12366
+ return void 0;
12367
+ }
12368
+ }
12369
+ }
12370
+ }
12371
+ return void 0;
12372
+ }
12373
+ function parseJudgeResult(text) {
12374
+ const payload = extractFirstJsonObject(text);
12375
+ if (!payload) {
12376
+ return void 0;
12377
+ }
12378
+ const rawScore = payload.score ?? payload.judgeScore;
12379
+ const score = typeof rawScore === "number" ? rawScore : typeof rawScore === "string" && rawScore.trim() ? Number(rawScore) : Number.NaN;
12380
+ if (!Number.isFinite(score) || score < 0 || score > 1) {
12381
+ return void 0;
12382
+ }
12383
+ const rawFindings = payload.findings ?? payload.judgeFindings;
12384
+ const judgeFindings = Array.isArray(rawFindings) ? rawFindings.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim()) : typeof rawFindings === "string" && rawFindings.trim() ? [rawFindings.trim()] : void 0;
12385
+ const rawNotes = payload.notes ?? payload.calibrationNotes;
12386
+ return {
12387
+ judgeScore: Number(score.toFixed(4)),
12388
+ calibrationNotes: typeof rawNotes === "string" && rawNotes.trim() ? rawNotes.trim() : void 0,
12389
+ judgeFindings
12390
+ };
12391
+ }
12392
+ function evaluateDimension(dimension, output3, normalized) {
12393
+ const findings = [];
12394
+ const weight = Number.isFinite(dimension.weight) && (dimension.weight ?? 0) > 0 ? dimension.weight : 1;
12395
+ let score = 1;
12396
+ if ((dimension.minOutputChars ?? 0) > 0 && output3.trim().length < (dimension.minOutputChars ?? 0)) {
12397
+ findings.push(`output_too_short:${output3.trim().length}/${dimension.minOutputChars}`);
12398
+ score -= 0.25;
12399
+ }
12400
+ const requiredKeywords = dimension.requiredKeywords ?? [];
12401
+ const missingKeywords = requiredKeywords.filter((keyword) => !normalized.includes(keyword.toLowerCase()));
12402
+ if (missingKeywords.length) {
12403
+ findings.push(`missing_keywords:${missingKeywords.join("|")}`);
12404
+ score -= 0.6 * rate2(missingKeywords.length, Math.max(requiredKeywords.length, 1));
12405
+ }
12406
+ if (dimension.requiresCodeBlock && !includesCodeBlock(output3)) {
12407
+ findings.push("missing_code_block");
12408
+ score -= 0.25;
12409
+ }
12410
+ const forbiddenMatches = (dimension.forbiddenPatterns ?? []).filter((pattern) => normalized.includes(pattern.toLowerCase()));
12411
+ if (forbiddenMatches.length) {
12412
+ findings.push(`forbidden_patterns:${forbiddenMatches.join("|")}`);
12413
+ score -= 0.5;
12414
+ }
12415
+ return {
12416
+ id: normalizeDimensionId(dimension.id),
12417
+ label: dimension.label ?? dimension.id,
12418
+ score: clamp(score),
12419
+ weight,
12420
+ findings
12421
+ };
12422
+ }
12423
+ function weightedAverageDimensionScore(scores) {
12424
+ const totalWeight = scores.reduce((sum, score) => sum + score.weight, 0);
12425
+ if (!scores.length || totalWeight <= 0) {
12426
+ return 1;
12427
+ }
12428
+ return clamp(scores.reduce((sum, score) => sum + score.score * score.weight, 0) / totalWeight);
12429
+ }
12430
+ function averageDimensionScores(runs) {
12431
+ const grouped = {};
12432
+ for (const run2 of runs) {
12433
+ for (const dimension of run2.dimensionScores) {
12434
+ grouped[dimension.id] ??= [];
12435
+ grouped[dimension.id].push(dimension.score);
12436
+ }
12437
+ }
12438
+ return Object.fromEntries(
12439
+ Object.entries(grouped).map(([id, values]) => [id, average2(values)]).sort(([left], [right]) => left.localeCompare(right))
12440
+ );
12441
+ }
12442
+ function buildCalibration(input3, qualityScore) {
12443
+ const scores = [input3.humanScore, input3.judgeScore].filter((value) => typeof value === "number");
12444
+ const findings = [...input3.judgeFindings ?? []];
12445
+ if (!scores.length && !input3.calibrationNotes && !findings.length) {
12446
+ return void 0;
12447
+ }
12448
+ const averageScore = scores.length ? average2(scores) : void 0;
12449
+ const deltaFromQuality = averageScore === void 0 ? void 0 : Number((averageScore - qualityScore).toFixed(4));
12450
+ if (deltaFromQuality !== void 0 && Math.abs(deltaFromQuality) >= 0.25) {
12451
+ findings.push(`calibration_disagreement:${deltaFromQuality}`);
12452
+ }
12453
+ return {
12454
+ humanScore: input3.humanScore,
12455
+ judgeScore: input3.judgeScore,
12456
+ averageScore,
12457
+ deltaFromQuality,
12458
+ notes: input3.calibrationNotes,
12459
+ findings
12460
+ };
12461
+ }
12462
+ function evaluateRun(task, input3) {
12463
+ const findings = [];
12464
+ const output3 = input3.output ?? "";
12465
+ const normalized = normalizeText(output3);
12466
+ const minQualityScore = task.minQualityScore ?? 0.7;
12467
+ let qualityScore = 1;
12468
+ let dimensionsPassed = true;
12469
+ if (input3.error) {
12470
+ findings.push(`runner_error:${input3.error}`);
12471
+ qualityScore = 0;
12472
+ }
12473
+ if (input3.judgeError) {
12474
+ findings.push(`judge_error:${input3.judgeError}`);
12475
+ }
12476
+ const dimensions = qualityDimensionsForTask(task);
12477
+ const dimensionScores = dimensions.map((dimension) => evaluateDimension(dimension, output3, normalized));
12478
+ for (const dimension of dimensionScores) {
12479
+ const sourceDimension = dimensions.find((item) => normalizeDimensionId(item.id) === dimension.id);
12480
+ const minScore = sourceDimension?.minScore ?? 0.7;
12481
+ if (dimension.score < minScore) {
12482
+ dimensionsPassed = false;
12483
+ findings.push(`dimension_below_threshold:${dimension.id}:${dimension.score}/${minScore}`);
12484
+ }
12485
+ for (const finding of dimension.findings) {
12486
+ findings.push(`dimension_${dimension.id}:${finding}`);
12487
+ }
12488
+ }
12489
+ if ((task.minOutputChars ?? 0) > 0 && output3.trim().length < (task.minOutputChars ?? 0)) {
12490
+ findings.push(`output_too_short:${output3.trim().length}/${task.minOutputChars}`);
12491
+ qualityScore -= 0.25;
12492
+ }
12493
+ const requiredKeywords = task.requiredKeywords ?? [];
12494
+ const missingKeywords = requiredKeywords.filter((keyword) => !normalized.includes(keyword.toLowerCase()));
12495
+ if (missingKeywords.length) {
12496
+ findings.push(`missing_keywords:${missingKeywords.join("|")}`);
12497
+ qualityScore -= 0.35 * rate2(missingKeywords.length, Math.max(requiredKeywords.length, 1));
12498
+ }
12499
+ if (task.requiresCodeBlock && !includesCodeBlock(output3)) {
12500
+ findings.push("missing_code_block");
12501
+ qualityScore -= 0.25;
12502
+ }
12503
+ const forbiddenMatches = (task.forbiddenPatterns ?? []).filter((pattern) => normalized.includes(pattern.toLowerCase()));
12504
+ if (forbiddenMatches.length) {
12505
+ findings.push(`forbidden_patterns:${forbiddenMatches.join("|")}`);
12506
+ qualityScore -= 0.4;
12507
+ }
12508
+ const latencyMs = typeof input3.latencyMs === "number" ? input3.latencyMs : void 0;
12509
+ const speedScore = latencyMs === void 0 || !task.maxLatencyMs ? 0 : clamp(1 - Math.max(0, latencyMs - task.maxLatencyMs) / task.maxLatencyMs);
12510
+ if (latencyMs !== void 0 && task.maxLatencyMs && latencyMs > task.maxLatencyMs) {
12511
+ findings.push(`latency_over_budget:${latencyMs}/${task.maxLatencyMs}`);
12512
+ }
12513
+ const finalQualityScore = clamp(Math.min(qualityScore, weightedAverageDimensionScore(dimensionScores)));
12514
+ const calibration = buildCalibration(input3, finalQualityScore);
12515
+ if (calibration) {
12516
+ for (const finding of calibration.findings) {
12517
+ findings.push(`calibration:${finding}`);
12518
+ }
12519
+ }
12520
+ return {
12521
+ taskId: task.id,
12522
+ intent: task.intent,
12523
+ model: input3.model,
12524
+ passed: !input3.error && dimensionsPassed && finalQualityScore >= minQualityScore,
12525
+ qualityScore: finalQualityScore,
12526
+ speedScore,
12527
+ latencyMs,
12528
+ dimensionScores,
12529
+ calibration,
12530
+ findings
12531
+ };
12532
+ }
12533
+ function summarizeGroup(key, runs) {
12534
+ const latencies = runs.map((run2) => run2.latencyMs).filter((value) => typeof value === "number");
12535
+ return {
12536
+ key,
12537
+ totalRuns: runs.length,
12538
+ passedRuns: runs.filter((run2) => run2.passed).length,
12539
+ passRate: rate2(runs.filter((run2) => run2.passed).length, runs.length),
12540
+ averageQualityScore: average2(runs.map((run2) => run2.qualityScore)),
12541
+ averageSpeedScore: average2(runs.map((run2) => run2.speedScore)),
12542
+ averageLatencyMs: latencies.length ? Number(average2(latencies).toFixed(2)) : 0,
12543
+ averageDimensionScores: averageDimensionScores(runs)
12544
+ };
12545
+ }
12546
+ function groupRuns(runs, keyFn) {
12547
+ const groups = {};
12548
+ for (const run2 of runs) {
12549
+ const key = keyFn(run2);
12550
+ groups[key] ??= [];
12551
+ groups[key].push(run2);
12552
+ }
12553
+ return Object.entries(groups).map(([key, groupRunsForKey]) => summarizeGroup(key, groupRunsForKey)).sort((left, right) => {
12554
+ if (right.passRate !== left.passRate) {
12555
+ return right.passRate - left.passRate;
12556
+ }
12557
+ if (right.averageQualityScore !== left.averageQualityScore) {
12558
+ return right.averageQualityScore - left.averageQualityScore;
12559
+ }
12560
+ return left.averageLatencyMs - right.averageLatencyMs;
12561
+ });
12562
+ }
12563
+ function bestRunForTask(taskId, runs) {
12564
+ return runs.filter((run2) => run2.taskId === taskId).sort((left, right) => {
12565
+ if (Number(right.passed) !== Number(left.passed)) {
12566
+ return Number(right.passed) - Number(left.passed);
12567
+ }
12568
+ if (right.qualityScore !== left.qualityScore) {
12569
+ return right.qualityScore - left.qualityScore;
12570
+ }
12571
+ if (right.speedScore !== left.speedScore) {
12572
+ return right.speedScore - left.speedScore;
12573
+ }
12574
+ return (left.latencyMs ?? Number.POSITIVE_INFINITY) - (right.latencyMs ?? Number.POSITIVE_INFINITY);
12575
+ })[0];
12576
+ }
12577
+ function summarizeCalibration(runs) {
12578
+ const calibratedRuns = runs.filter((run2) => run2.calibration);
12579
+ const humanScores = calibratedRuns.map((run2) => run2.calibration?.humanScore).filter((value) => typeof value === "number");
12580
+ const judgeScores = calibratedRuns.map((run2) => run2.calibration?.judgeScore).filter((value) => typeof value === "number");
12581
+ const averageScores = calibratedRuns.map((run2) => run2.calibration?.averageScore).filter((value) => typeof value === "number");
12582
+ const deltas = calibratedRuns.map((run2) => run2.calibration?.deltaFromQuality).filter((value) => typeof value === "number");
12583
+ const highDisagreementRuns = calibratedRuns.filter((run2) => Math.abs(run2.calibration?.deltaFromQuality ?? 0) >= 0.25).map((run2) => ({
12584
+ taskId: run2.taskId,
12585
+ model: run2.model,
12586
+ qualityScore: run2.qualityScore,
12587
+ calibrationScore: run2.calibration?.averageScore ?? 0,
12588
+ deltaFromQuality: run2.calibration?.deltaFromQuality ?? 0,
12589
+ findings: run2.calibration?.findings ?? []
12590
+ }));
12591
+ return {
12592
+ calibratedRuns: calibratedRuns.length,
12593
+ averageHumanScore: average2(humanScores),
12594
+ averageJudgeScore: average2(judgeScores),
12595
+ averageCalibrationScore: average2(averageScores),
12596
+ averageRubricDelta: average2(deltas),
12597
+ highDisagreementRuns
12598
+ };
12599
+ }
12600
+ function runOfflineTaskEvaluation(inputs, tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
12601
+ const taskMap = new Map(tasks.map((task) => [task.id, task]));
12602
+ const missingTaskIds = Array.from(new Set(inputs.map((input3) => input3.taskId).filter((taskId) => !taskMap.has(taskId))));
12603
+ const runs = inputs.map((input3) => {
12604
+ const task = taskMap.get(input3.taskId);
12605
+ return task ? evaluateRun(task, input3) : void 0;
12606
+ }).filter((run2) => Boolean(run2));
12607
+ const latencies = runs.map((run2) => run2.latencyMs).filter((value) => typeof value === "number");
12608
+ const bestRunsByTask = tasks.map((task) => bestRunForTask(task.id, runs)).filter((run2) => Boolean(run2));
12609
+ return {
12610
+ totalTasks: tasks.length,
12611
+ totalRuns: inputs.length,
12612
+ evaluatedRuns: runs.length,
12613
+ missingTaskIds,
12614
+ passRate: rate2(runs.filter((run2) => run2.passed).length, runs.length),
12615
+ averageQualityScore: average2(runs.map((run2) => run2.qualityScore)),
12616
+ averageSpeedScore: average2(runs.map((run2) => run2.speedScore)),
12617
+ averageLatencyMs: latencies.length ? Number(average2(latencies).toFixed(2)) : 0,
12618
+ averageDimensionScores: averageDimensionScores(runs),
12619
+ calibrationSummary: summarizeCalibration(runs),
12620
+ bestRunsByTask,
12621
+ byTask: groupRuns(runs, (run2) => run2.taskId),
12622
+ byModel: groupRuns(runs, (run2) => run2.model),
12623
+ runs
12624
+ };
12625
+ }
12626
+ async function runBenchmarkJob(task, model, options) {
12627
+ const startedAt = Date.now();
12628
+ const url = `${options.baseUrl.replace(/\/+$/, "")}/v1/messages`;
12629
+ try {
12630
+ const response = await options.fetchFn(url, {
12631
+ method: "POST",
12632
+ headers: {
12633
+ "Content-Type": "application/json",
12634
+ "anthropic-version": "2023-06-01",
12635
+ ...options.apiKey ? { Authorization: `Bearer ${options.apiKey}`, "x-api-key": options.apiKey } : {}
12636
+ },
12637
+ body: JSON.stringify({
12638
+ model,
12639
+ max_tokens: options.maxTokens,
12640
+ stream: false,
12641
+ metadata: {
12642
+ ctr_eval_task_id: task.id,
12643
+ ctr_eval_intent: task.intent
12644
+ },
12645
+ messages: [
12646
+ {
12647
+ role: "user",
12648
+ content: task.prompt
12649
+ }
12650
+ ]
12651
+ }),
12652
+ ...options.timeoutMs > 0 ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
12653
+ });
12654
+ const latencyMs = Date.now() - startedAt;
12655
+ if (!response.ok) {
12656
+ return {
12657
+ taskId: task.id,
12658
+ model,
12659
+ latencyMs,
12660
+ error: `http_${response.status}`
12661
+ };
12662
+ }
12663
+ const payload = await response.json();
12664
+ return {
12665
+ taskId: task.id,
12666
+ model,
12667
+ latencyMs,
12668
+ output: extractResponseText(payload)
12669
+ };
12670
+ } catch (error) {
12671
+ return {
12672
+ taskId: task.id,
12673
+ model,
12674
+ latencyMs: Date.now() - startedAt,
12675
+ error: error?.name === "TimeoutError" ? "timeout" : error?.message || "request_failed"
12676
+ };
12677
+ }
12678
+ }
12679
+ function buildJudgePrompt(task, input3) {
12680
+ return [
12681
+ "You are judging a Claude Trigger Router fixed-task benchmark result.",
12682
+ "Return only compact JSON with this exact shape:",
12683
+ '{"score":0.0,"findings":["short finding"],"notes":"short rationale"}',
12684
+ "Score must be a number from 0 to 1. Do not include markdown.",
12685
+ "",
12686
+ `Task id: ${task.id}`,
12687
+ `Intent: ${task.intent}`,
12688
+ `Expected output: ${task.expectedOutput ?? "A complete answer that satisfies the task prompt."}`,
12689
+ `Prompt: ${task.prompt}`,
12690
+ `Candidate model: ${input3.model}`,
12691
+ "",
12692
+ "Candidate output:",
12693
+ input3.output ?? ""
12694
+ ].join("\n");
12695
+ }
12696
+ async function runJudgeJob(task, input3, options) {
12697
+ if (input3.error) {
12698
+ return input3;
12699
+ }
12700
+ if (!task) {
12701
+ return {
12702
+ ...input3,
12703
+ judgeError: "unknown_task"
12704
+ };
12705
+ }
12706
+ if (!input3.output?.trim()) {
12707
+ return {
12708
+ ...input3,
12709
+ judgeError: "missing_output"
12710
+ };
12711
+ }
12712
+ const url = `${options.baseUrl.replace(/\/+$/, "")}/v1/messages`;
12713
+ try {
12714
+ const response = await options.fetchFn(url, {
12715
+ method: "POST",
12716
+ headers: {
12717
+ "Content-Type": "application/json",
12718
+ "anthropic-version": "2023-06-01",
12719
+ ...options.apiKey ? { Authorization: `Bearer ${options.apiKey}`, "x-api-key": options.apiKey } : {}
12720
+ },
12721
+ body: JSON.stringify({
12722
+ model: options.judgeModel,
12723
+ max_tokens: options.maxTokens,
12724
+ stream: false,
12725
+ metadata: {
12726
+ ctr_eval_judge_task_id: task.id,
12727
+ ctr_eval_judge_model: options.judgeModel,
12728
+ ctr_eval_judge_target_model: input3.model
12729
+ },
12730
+ messages: [
12731
+ {
12732
+ role: "user",
12733
+ content: buildJudgePrompt(task, input3)
12734
+ }
12735
+ ]
12736
+ }),
12737
+ ...options.timeoutMs > 0 ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
12738
+ });
12739
+ if (!response.ok) {
12740
+ return {
12741
+ ...input3,
12742
+ judgeError: `http_${response.status}`
12743
+ };
12744
+ }
12745
+ const payload = await response.json();
12746
+ const parsed = parseJudgeResult(extractResponseText(payload));
12747
+ if (!parsed) {
12748
+ return {
12749
+ ...input3,
12750
+ judgeError: "invalid_response"
12751
+ };
12752
+ }
12753
+ return {
12754
+ ...input3,
12755
+ judgeError: void 0,
12756
+ judgeScore: parsed.judgeScore,
12757
+ calibrationNotes: parsed.calibrationNotes ?? input3.calibrationNotes,
12758
+ judgeFindings: parsed.judgeFindings ?? input3.judgeFindings
12759
+ };
12760
+ } catch (error) {
12761
+ return {
12762
+ ...input3,
12763
+ judgeError: error?.name === "TimeoutError" ? "timeout" : error?.message || "request_failed"
12764
+ };
12765
+ }
12766
+ }
12767
+ async function runOfflineTaskJudge(options) {
12768
+ const tasks = options.tasks ?? DEFAULT_OFFLINE_EVALUATION_TASKS;
12769
+ const taskMap = new Map(tasks.map((task) => [task.id, task]));
12770
+ const judgeModel = options.judgeModel.trim();
12771
+ if (!judgeModel) {
12772
+ throw new Error("LLM \u88C1\u5224\u9700\u8981 judgeModel\u3002");
12773
+ }
12774
+ if (!options.baseUrl?.trim()) {
12775
+ throw new Error("LLM \u88C1\u5224\u9700\u8981 baseUrl\u3002");
12776
+ }
12777
+ const inputs = new Array(options.inputs.length);
12778
+ let nextIndex = 0;
12779
+ const concurrency = Math.max(1, Math.min(Math.floor(options.concurrency ?? 2), 8));
12780
+ const sharedOptions = {
12781
+ judgeModel,
12782
+ baseUrl: options.baseUrl.trim(),
12783
+ apiKey: options.apiKey?.trim() || void 0,
12784
+ timeoutMs: Math.max(0, Math.floor(options.timeoutMs ?? 3e4)),
12785
+ maxTokens: Math.max(1, Math.floor(options.maxTokens ?? 256)),
12786
+ fetchFn: options.fetchFn ?? fetch
12787
+ };
12788
+ async function worker() {
12789
+ while (nextIndex < options.inputs.length) {
12790
+ const currentIndex = nextIndex;
12791
+ nextIndex += 1;
12792
+ const input3 = options.inputs[currentIndex];
12793
+ inputs[currentIndex] = await runJudgeJob(taskMap.get(input3.taskId), input3, sharedOptions);
12794
+ }
12795
+ }
12796
+ await Promise.all(Array.from({ length: Math.min(concurrency, options.inputs.length) }, () => worker()));
12797
+ return {
12798
+ inputs,
12799
+ report: runOfflineTaskEvaluation(inputs, tasks)
12800
+ };
12801
+ }
12802
+ async function runOfflineTaskBenchmark(options) {
12803
+ const tasks = options.tasks ?? DEFAULT_OFFLINE_EVALUATION_TASKS;
12804
+ const models = options.models.map((model) => model.trim()).filter(Boolean);
12805
+ if (!models.length) {
12806
+ throw new Error("\u81F3\u5C11\u9700\u8981\u63D0\u4F9B\u4E00\u4E2A\u6A21\u578B\u7528\u4E8E\u81EA\u52A8\u8BC4\u6D4B\u3002");
12807
+ }
12808
+ if (!options.baseUrl?.trim()) {
12809
+ throw new Error("\u81EA\u52A8\u8BC4\u6D4B\u9700\u8981 baseUrl\u3002");
12810
+ }
12811
+ const jobs = tasks.flatMap((task) => models.map((model) => ({ task, model })));
12812
+ const inputs = new Array(jobs.length);
12813
+ let nextIndex = 0;
12814
+ const concurrency = Math.max(1, Math.min(Math.floor(options.concurrency ?? 2), 8));
12815
+ const sharedOptions = {
12816
+ baseUrl: options.baseUrl.trim(),
12817
+ apiKey: options.apiKey?.trim() || void 0,
12818
+ timeoutMs: Math.max(0, Math.floor(options.timeoutMs ?? 3e4)),
12819
+ maxTokens: Math.max(1, Math.floor(options.maxTokens ?? 768)),
12820
+ fetchFn: options.fetchFn ?? fetch
12821
+ };
12822
+ async function worker() {
12823
+ while (nextIndex < jobs.length) {
12824
+ const currentIndex = nextIndex;
12825
+ nextIndex += 1;
12826
+ const job = jobs[currentIndex];
12827
+ inputs[currentIndex] = await runBenchmarkJob(job.task, job.model, sharedOptions);
12828
+ }
12829
+ }
12830
+ await Promise.all(Array.from({ length: Math.min(concurrency, jobs.length) }, () => worker()));
12831
+ if (options.judgeModel?.trim()) {
12832
+ return runOfflineTaskJudge({
12833
+ inputs,
12834
+ tasks,
12835
+ judgeModel: options.judgeModel,
12836
+ baseUrl: sharedOptions.baseUrl,
12837
+ apiKey: sharedOptions.apiKey,
12838
+ timeoutMs: sharedOptions.timeoutMs,
12839
+ concurrency,
12840
+ maxTokens: options.judgeMaxTokens ?? 256,
12841
+ fetchFn: sharedOptions.fetchFn
12842
+ });
12843
+ }
12844
+ return {
12845
+ inputs,
12846
+ report: runOfflineTaskEvaluation(inputs, tasks)
12847
+ };
12848
+ }
12849
+ function buildOfflineTaskManifest(tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
12850
+ return {
12851
+ version: 1,
12852
+ description: "Fixed task set for repeatable Claude Trigger Router model-combination evaluation.",
12853
+ tasks: tasks.map((task) => ({
12854
+ id: task.id,
12855
+ intent: task.intent,
12856
+ category: task.category ?? "general",
12857
+ prompt: task.prompt,
12858
+ expectedOutput: task.expectedOutput ?? "A complete answer that satisfies the task prompt.",
12859
+ rubric: {
12860
+ minQualityScore: task.minQualityScore ?? 0.7,
12861
+ minOutputChars: task.minOutputChars ?? 0,
12862
+ maxLatencyMs: task.maxLatencyMs,
12863
+ requiredKeywords: task.requiredKeywords ?? [],
12864
+ forbiddenPatterns: task.forbiddenPatterns ?? [],
12865
+ requiresCodeBlock: Boolean(task.requiresCodeBlock),
12866
+ qualityDimensions: qualityDimensionsForTask(task).map((dimension) => ({
12867
+ id: normalizeDimensionId(dimension.id),
12868
+ label: dimension.label ?? dimension.id,
12869
+ weight: dimension.weight ?? 1,
12870
+ minScore: dimension.minScore ?? 0.7,
12871
+ minOutputChars: dimension.minOutputChars,
12872
+ requiredKeywords: dimension.requiredKeywords ?? [],
12873
+ forbiddenPatterns: dimension.forbiddenPatterns ?? [],
12874
+ requiresCodeBlock: Boolean(dimension.requiresCodeBlock)
12875
+ }))
12876
+ },
12877
+ resultTemplate: {
12878
+ taskId: task.id,
12879
+ model: "<provider,model>",
12880
+ output: "<model output>",
12881
+ latencyMs: 0,
12882
+ humanScore: null,
12883
+ judgeScore: null,
12884
+ calibrationNotes: null,
12885
+ judgeFindings: []
12886
+ }
12887
+ }))
12888
+ };
12889
+ }
12890
+ function formatOfflineTaskManifest(tasks = DEFAULT_OFFLINE_EVALUATION_TASKS) {
12891
+ const lines = [
12892
+ "Offline evaluation tasks",
12893
+ `Total tasks: ${tasks.length}`
12894
+ ];
12895
+ for (const task of tasks) {
12896
+ lines.push(`- ${task.id} [${task.intent}/${task.category ?? "general"}]`);
12897
+ lines.push(` Prompt: ${task.prompt}`);
12898
+ lines.push(` Expected: ${task.expectedOutput ?? "A complete answer that satisfies the task prompt."}`);
12899
+ lines.push(` Rubric: minQuality=${task.minQualityScore ?? 0.7}, minChars=${task.minOutputChars ?? 0}, maxLatencyMs=${task.maxLatencyMs ?? "-"}`);
12900
+ lines.push(` Required: ${(task.requiredKeywords ?? []).join("|") || "-"}`);
12901
+ lines.push(` Forbidden: ${(task.forbiddenPatterns ?? []).join("|") || "-"}`);
12902
+ lines.push(` Requires code block: ${Boolean(task.requiresCodeBlock)}`);
12903
+ lines.push(` Dimensions: ${qualityDimensionsForTask(task).map((dimension) => normalizeDimensionId(dimension.id)).join("|") || "-"}`);
12904
+ }
12905
+ return lines.join("\n");
12906
+ }
12907
+ function formatDimensionSummary(scores) {
12908
+ const entries = Object.entries(scores);
12909
+ if (!entries.length) {
12910
+ return "-";
12911
+ }
12912
+ return entries.map(([id, score]) => `${id}=${score.toFixed(2)}`).join(", ");
12913
+ }
12914
+ function formatOfflineTaskEvaluationReport(report) {
12915
+ const lines = [
12916
+ "Offline routing evaluation",
12917
+ `Tasks: ${report.totalTasks}, runs: ${report.evaluatedRuns}/${report.totalRuns}, passRate: ${(report.passRate * 100).toFixed(1)}%`,
12918
+ `Average quality: ${report.averageQualityScore.toFixed(2)}, speed: ${report.averageSpeedScore.toFixed(2)}, latency: ${report.averageLatencyMs} ms`
12919
+ ];
12920
+ const dimensions = Object.entries(report.averageDimensionScores);
12921
+ if (dimensions.length) {
12922
+ lines.push(`Average dimensions: ${formatDimensionSummary(report.averageDimensionScores)}`);
12923
+ }
12924
+ if (report.calibrationSummary.calibratedRuns) {
12925
+ lines.push(
12926
+ `Calibration: ${report.calibrationSummary.calibratedRuns} runs, human ${report.calibrationSummary.averageHumanScore.toFixed(2)}, judge ${report.calibrationSummary.averageJudgeScore.toFixed(2)}, delta ${report.calibrationSummary.averageRubricDelta.toFixed(2)}`
12927
+ );
12928
+ } else {
12929
+ lines.push("Calibration: none (add humanScore or judgeScore to compare rubric with human/LLM judge)");
12930
+ }
12931
+ if (report.missingTaskIds.length) {
12932
+ lines.push(`Missing task ids: ${report.missingTaskIds.join(", ")}`);
12933
+ }
12934
+ lines.push("By model:");
12935
+ for (const item of report.byModel) {
12936
+ lines.push(`- ${item.key}: pass ${(item.passRate * 100).toFixed(1)}%, quality ${item.averageQualityScore.toFixed(2)}, latency ${item.averageLatencyMs} ms, dimensions ${formatDimensionSummary(item.averageDimensionScores)}`);
12937
+ }
12938
+ lines.push("Best runs by task:");
12939
+ for (const run2 of report.bestRunsByTask) {
12940
+ lines.push(`- ${run2.taskId} -> ${run2.model}: ${run2.passed ? "pass" : "fail"}, quality ${run2.qualityScore.toFixed(2)}, latency ${run2.latencyMs ?? "-"} ms`);
12941
+ }
12942
+ const failedRuns = report.runs.filter((run2) => !run2.passed || run2.findings.length);
12943
+ if (failedRuns.length) {
12944
+ lines.push("Findings:");
12945
+ for (const run2 of failedRuns) {
12946
+ lines.push(`- ${run2.taskId} -> ${run2.model}: ${run2.findings.length ? run2.findings.join(", ") : "quality_below_threshold"}`);
12947
+ }
12948
+ }
12949
+ if (report.calibrationSummary.highDisagreementRuns.length) {
12950
+ lines.push("Calibration disagreements:");
12951
+ for (const run2 of report.calibrationSummary.highDisagreementRuns) {
12952
+ lines.push(`- ${run2.taskId} -> ${run2.model}: rubric ${run2.qualityScore.toFixed(2)}, calibration ${run2.calibrationScore.toFixed(2)}, delta ${run2.deltaFromQuality.toFixed(2)}`);
12953
+ }
12954
+ }
12955
+ return lines.join("\n");
12956
+ }
12957
+ var DEFAULT_OFFLINE_EVALUATION_TASKS;
12958
+ var init_task_evaluation = __esm({
12959
+ "src/governance/task-evaluation.ts"() {
12960
+ "use strict";
12961
+ DEFAULT_OFFLINE_EVALUATION_TASKS = [
12962
+ {
12963
+ id: "quick_status",
12964
+ intent: "quick_reply",
12965
+ category: "speed",
12966
+ prompt: "Summarize the current service status and next action in two concise sentences.",
12967
+ expectedOutput: "A brief status summary with a concrete next action.",
12968
+ maxLatencyMs: 800,
12969
+ minOutputChars: 40,
12970
+ requiredKeywords: ["status", "next"],
12971
+ forbiddenPatterns: ["TODO", "placeholder", "I cannot"]
12972
+ },
12973
+ {
12974
+ id: "coding_fix",
12975
+ intent: "coding",
12976
+ category: "quality",
12977
+ prompt: "Fix a TypeScript regression and explain the changed behavior with a test plan.",
12978
+ expectedOutput: "A concise fix explanation, a TypeScript code block, and a focused test plan.",
12979
+ maxLatencyMs: 1800,
12980
+ minOutputChars: 120,
12981
+ requiredKeywords: ["fix", "test"],
12982
+ forbiddenPatterns: ["TODO", "...rest of code", "placeholder"],
12983
+ requiresCodeBlock: true
12984
+ },
12985
+ {
12986
+ id: "architecture_review",
12987
+ intent: "architecture",
12988
+ category: "quality",
12989
+ prompt: "Review a router architecture change and list risks, tradeoffs, and rollout checks.",
12990
+ expectedOutput: "A structured architecture review that names risks, tradeoffs, and rollout checks.",
12991
+ maxLatencyMs: 2600,
12992
+ minOutputChars: 160,
12993
+ requiredKeywords: ["risk", "tradeoff", "rollout"],
12994
+ forbiddenPatterns: ["TODO", "placeholder"]
12995
+ },
12996
+ {
12997
+ id: "long_context_triage",
12998
+ intent: "long_context",
12999
+ category: "continuity",
13000
+ prompt: "Triage a long conversation and preserve the user goal, constraints, and open blockers.",
13001
+ expectedOutput: "A continuity-preserving summary with goal, constraints, and blockers.",
13002
+ maxLatencyMs: 3200,
13003
+ minOutputChars: 180,
13004
+ requiredKeywords: ["goal", "constraint", "blocker"],
13005
+ forbiddenPatterns: ["lost context", "cannot access previous"]
13006
+ },
13007
+ {
13008
+ id: "auth_deployment_plan",
13009
+ intent: "security",
13010
+ category: "server_ops",
13011
+ prompt: "Create a safe remote server deployment checklist for an LLM router with API key scope, rotation, audit, and rollback.",
13012
+ expectedOutput: "An operator checklist covering scoped keys, rotation, audit, and rollback.",
13013
+ maxLatencyMs: 2600,
13014
+ minOutputChars: 180,
13015
+ requiredKeywords: ["scope", "rotation", "audit", "rollback"],
13016
+ forbiddenPatterns: ["disable auth", "share the admin key", "placeholder"]
13017
+ },
13018
+ {
13019
+ id: "model_pool_incident",
13020
+ intent: "operations",
13021
+ category: "pool_health",
13022
+ prompt: "Diagnose a model pool incident where one endpoint is slow and another returns intermittent 5xx errors; propose routing actions.",
13023
+ expectedOutput: "A pool health diagnosis with latency, 5xx, fallback or circuit breaker actions.",
13024
+ maxLatencyMs: 2200,
13025
+ minOutputChars: 160,
13026
+ requiredKeywords: ["latency", "5xx", "fallback"],
13027
+ forbiddenPatterns: ["TODO", "placeholder"]
13028
+ }
13029
+ ];
10716
13030
  }
10717
13031
  });
10718
13032
 
@@ -10725,7 +13039,7 @@ __export(cli_exports, {
10725
13039
  });
10726
13040
  module.exports = __toCommonJS(cli_exports);
10727
13041
  function getPackageInfo() {
10728
- const content = (0, import_fs9.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
13042
+ const content = (0, import_fs10.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
10729
13043
  const pkg = JSON.parse(content);
10730
13044
  return {
10731
13045
  name: pkg.name ?? "@peterwangze/claude-trigger-router",
@@ -10745,7 +13059,18 @@ function hasArg2(flag, shortFlag) {
10745
13059
  function getArgValue(flag, shortFlag) {
10746
13060
  const args = getArgs();
10747
13061
  const index = args.indexOf(flag) !== -1 ? args.indexOf(flag) : shortFlag ? args.indexOf(shortFlag) : -1;
10748
- return index !== -1 ? args[index + 1] : void 0;
13062
+ const value = index !== -1 ? args[index + 1] : void 0;
13063
+ return value && !value.startsWith("-") ? value : void 0;
13064
+ }
13065
+ function getOptionalArgValue(flag, label) {
13066
+ if (!hasArg2(flag)) {
13067
+ return void 0;
13068
+ }
13069
+ const value = getArgValue(flag);
13070
+ if (!value) {
13071
+ throw new Error(`${label} \u9700\u8981\u63D0\u4F9B\u503C\uFF1A${flag} <value>`);
13072
+ }
13073
+ return value;
10749
13074
  }
10750
13075
  function parsePortValue(portValue, sourceLabel) {
10751
13076
  const trimmed = portValue.trim();
@@ -10765,16 +13090,16 @@ function getPort() {
10765
13090
  }
10766
13091
  try {
10767
13092
  const yaml4 = require("js-yaml");
10768
- if ((0, import_fs9.existsSync)(CONFIG_FILE)) {
10769
- const content = (0, import_fs9.readFileSync)(CONFIG_FILE, "utf-8");
13093
+ if ((0, import_fs10.existsSync)(CONFIG_FILE)) {
13094
+ const content = (0, import_fs10.readFileSync)(CONFIG_FILE, "utf-8");
10770
13095
  const config = yaml4.load(content);
10771
13096
  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");
13097
+ } else if ((0, import_fs10.existsSync)(CONFIG_FILE_YML)) {
13098
+ const content = (0, import_fs10.readFileSync)(CONFIG_FILE_YML, "utf-8");
10774
13099
  const config = yaml4.load(content);
10775
13100
  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");
13101
+ } else if ((0, import_fs10.existsSync)(CONFIG_FILE_JSON)) {
13102
+ const content = (0, import_fs10.readFileSync)(CONFIG_FILE_JSON, "utf-8");
10778
13103
  const config = JSON.parse(content);
10779
13104
  if (config?.PORT) return config.PORT;
10780
13105
  }
@@ -10794,6 +13119,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
10794
13119
  \u547D\u4EE4\uFF1A
10795
13120
  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
13121
  doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
13122
+ eval \u79BB\u7EBF\u8BC4\u6D4B\u56FA\u5B9A\u4EFB\u52A1\u96C6\u8F93\u51FA\uFF08--input / --tasks / --run / --judge-model\uFF09
10797
13123
  init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
10798
13124
  deploy \u751F\u6210\u90E8\u7F72\u5165\u53E3\u914D\u7F6E\uFF08\u5F53\u524D\u652F\u6301 deploy init --target server\uFF09
10799
13125
  start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
@@ -10814,6 +13140,10 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
10814
13140
  \u4F7F\u7528\u793A\u4F8B\uFF1A
10815
13141
  ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
10816
13142
  ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
13143
+ ctr eval --tasks # \u67E5\u770B\u56FA\u5B9A\u8BC4\u6D4B\u4EFB\u52A1\u3001prompt \u548C rubric
13144
+ ctr eval --input results.json # \u7528\u56FA\u5B9A\u4EFB\u52A1\u96C6 rubric \u8BC4\u6D4B\u591A\u6A21\u578B\u8F93\u51FA\u7ED3\u679C
13145
+ ctr eval --run --models "sonnet;haiku" # \u81EA\u52A8\u8C03\u7528 CTR /v1/messages \u540E\u8BC4\u6D4B
13146
+ ctr eval --run --models "sonnet;haiku" --judge-model sonnet # \u81EA\u52A8\u8FFD\u52A0 LLM \u88C1\u5224\u5206
10817
13147
  ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
10818
13148
  ctr deploy init --target server # \u751F\u6210\u5B89\u5168\u9ED8\u8BA4\u7684 server \u90E8\u7F72\u914D\u7F6E
10819
13149
  ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
@@ -10841,10 +13171,10 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
10841
13171
  function readConfigForCliStatus() {
10842
13172
  const yaml4 = require("js-yaml");
10843
13173
  for (const configFile of [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON]) {
10844
- if (!(0, import_fs9.existsSync)(configFile)) {
13174
+ if (!(0, import_fs10.existsSync)(configFile)) {
10845
13175
  continue;
10846
13176
  }
10847
- const content = (0, import_fs9.readFileSync)(configFile, "utf-8");
13177
+ const content = (0, import_fs10.readFileSync)(configFile, "utf-8");
10848
13178
  return configFile.endsWith(".json") ? JSON.parse(content) : yaml4.load(content);
10849
13179
  }
10850
13180
  return {};
@@ -10905,6 +13235,111 @@ function printRuntimeStatus(config, port, liveInfo) {
10905
13235
  }
10906
13236
  console.log(` \u672C\u5730\u63A5\u5165\uFF1A${clientConnection?.baseUrl || `http://127.0.0.1:${listenerPort}`}`);
10907
13237
  }
13238
+ function readOfflineEvaluationInputs(inputPath) {
13239
+ const payload = JSON.parse((0, import_fs10.readFileSync)(inputPath, "utf-8"));
13240
+ return parseOfflineEvaluationInputs(payload);
13241
+ }
13242
+ function parsePositiveIntegerArg(flag, shortFlag, fallback, label) {
13243
+ const value = getArgValue(flag, shortFlag);
13244
+ if (!value) {
13245
+ return fallback;
13246
+ }
13247
+ if (!/^\d+$/.test(value)) {
13248
+ throw new Error(`${label} \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${value}`);
13249
+ }
13250
+ const parsed = Number.parseInt(value, 10);
13251
+ if (!Number.isInteger(parsed) || parsed < 1) {
13252
+ throw new Error(`${label} \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${value}`);
13253
+ }
13254
+ return parsed;
13255
+ }
13256
+ function parseEvalModelsArg() {
13257
+ const modelsValue = getArgValue("--models") || getArgValue("--model");
13258
+ return (modelsValue ?? "").split(";").map((item) => item.trim()).filter(Boolean);
13259
+ }
13260
+ async function runOfflineEvaluationCli() {
13261
+ if (hasArg2("--tasks")) {
13262
+ if (hasArg2("--json")) {
13263
+ console.log(JSON.stringify(buildOfflineTaskManifest(), null, 2));
13264
+ return;
13265
+ }
13266
+ console.log(formatOfflineTaskManifest());
13267
+ return;
13268
+ }
13269
+ if (hasArg2("--run")) {
13270
+ const models = parseEvalModelsArg();
13271
+ if (!models.length) {
13272
+ console.log('\u8BF7\u63D0\u4F9B\u81EA\u52A8\u8BC4\u6D4B\u6A21\u578B\uFF1Actr eval --run --models "sonnet;haiku"');
13273
+ 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");
13274
+ process.exit(1);
13275
+ }
13276
+ try {
13277
+ const config = readConfigForCliStatus();
13278
+ const baseUrl = getArgValue("--base-url") || `http://127.0.0.1:${getPort()}`;
13279
+ const apiKey = getArgValue("--api-key") || getLocalClaudeProxyToken(config);
13280
+ const judgeModel = getOptionalArgValue("--judge-model", "judge-model");
13281
+ const result = await runOfflineTaskBenchmark({
13282
+ models,
13283
+ baseUrl,
13284
+ apiKey,
13285
+ timeoutMs: parsePositiveIntegerArg("--timeout-ms", void 0, 3e4, "timeout-ms"),
13286
+ concurrency: parsePositiveIntegerArg("--concurrency", void 0, 2, "concurrency"),
13287
+ maxTokens: parsePositiveIntegerArg("--max-tokens", void 0, 768, "max-tokens"),
13288
+ judgeModel,
13289
+ judgeMaxTokens: parsePositiveIntegerArg("--judge-max-tokens", void 0, 256, "judge-max-tokens")
13290
+ });
13291
+ if (hasArg2("--json")) {
13292
+ console.log(JSON.stringify(result, null, 2));
13293
+ return;
13294
+ }
13295
+ console.log(formatOfflineTaskEvaluationReport(result.report));
13296
+ return;
13297
+ } catch (error) {
13298
+ console.error(`\u274C \u81EA\u52A8\u8BC4\u6D4B\u5931\u8D25\uFF1A${error.message}`);
13299
+ console.error(' \u793A\u4F8B\uFF1Actr eval --run --models "sonnet;haiku" --base-url http://127.0.0.1:5678');
13300
+ process.exit(1);
13301
+ }
13302
+ }
13303
+ const inputPath = getArgValue("--input", "-i");
13304
+ if (!inputPath) {
13305
+ console.log("\u8BF7\u63D0\u4F9B\u8BC4\u6D4B\u8F93\u5165\u6587\u4EF6\uFF1Actr eval --input results.json");
13306
+ console.log("\u53EF\u5148\u8FD0\u884C\uFF1Actr eval --tasks \u67E5\u770B\u56FA\u5B9A\u4EFB\u52A1\u3001prompt \u548C rubric");
13307
+ console.log('\u8F93\u5165\u683C\u5F0F\uFF1A[{ "taskId": "coding_fix", "model": "provider,model", "output": "...", "latencyMs": 1200 }]');
13308
+ process.exit(1);
13309
+ }
13310
+ try {
13311
+ const inputs = readOfflineEvaluationInputs(inputPath);
13312
+ const judgeModel = getOptionalArgValue("--judge-model", "judge-model");
13313
+ if (judgeModel) {
13314
+ const config = readConfigForCliStatus();
13315
+ const result = await runOfflineTaskJudge({
13316
+ inputs,
13317
+ judgeModel,
13318
+ baseUrl: getArgValue("--base-url") || `http://127.0.0.1:${getPort()}`,
13319
+ apiKey: getArgValue("--api-key") || getLocalClaudeProxyToken(config),
13320
+ timeoutMs: parsePositiveIntegerArg("--timeout-ms", void 0, 3e4, "timeout-ms"),
13321
+ concurrency: parsePositiveIntegerArg("--concurrency", void 0, 2, "concurrency"),
13322
+ maxTokens: parsePositiveIntegerArg("--judge-max-tokens", void 0, 256, "judge-max-tokens")
13323
+ });
13324
+ if (hasArg2("--json")) {
13325
+ console.log(JSON.stringify(result, null, 2));
13326
+ return;
13327
+ }
13328
+ console.log(formatOfflineTaskEvaluationReport(result.report));
13329
+ return;
13330
+ }
13331
+ const report = runOfflineTaskEvaluation(inputs);
13332
+ if (hasArg2("--json")) {
13333
+ console.log(JSON.stringify(report, null, 2));
13334
+ return;
13335
+ }
13336
+ console.log(formatOfflineTaskEvaluationReport(report));
13337
+ } catch (error) {
13338
+ console.error(`\u274C \u79BB\u7EBF\u8BC4\u6D4B\u5931\u8D25\uFF1A${error.message}`);
13339
+ console.error(" \u8BF7\u68C0\u67E5\u8F93\u5165\u683C\u5F0F\uFF1Actr eval --input results.json");
13340
+ process.exit(1);
13341
+ }
13342
+ }
10908
13343
  function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
10909
13344
  try {
10910
13345
  const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
@@ -10996,14 +13431,14 @@ function createBootstrapApiKey() {
10996
13431
  }
10997
13432
  function initConfig2() {
10998
13433
  const force = hasArg2("--force");
10999
- const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
13434
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs10.existsSync);
11000
13435
  if (existingConfig && !force) {
11001
13436
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
11002
13437
  console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
11003
13438
  return;
11004
13439
  }
11005
- if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
11006
- (0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
13440
+ if (!(0, import_fs10.existsSync)(CONFIG_DIR)) {
13441
+ (0, import_fs10.mkdirSync)(CONFIG_DIR, { recursive: true });
11007
13442
  }
11008
13443
  try {
11009
13444
  const yaml4 = require("js-yaml");
@@ -11013,7 +13448,7 @@ function initConfig2() {
11013
13448
  lineWidth: -1,
11014
13449
  noRefs: true
11015
13450
  });
11016
- (0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
13451
+ (0, import_fs10.writeFileSync)(CONFIG_FILE, content, "utf-8");
11017
13452
  const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
11018
13453
  console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
11019
13454
  console.log("");
@@ -11052,14 +13487,14 @@ function initDeployConfig() {
11052
13487
  printDeployHelp();
11053
13488
  process.exit(1);
11054
13489
  }
11055
- const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
13490
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs10.existsSync);
11056
13491
  if (existingConfig && !force) {
11057
13492
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
11058
13493
  console.log(" \u5982\u9700\u8986\u76D6\u90E8\u7F72\u6A21\u677F\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
11059
13494
  return;
11060
13495
  }
11061
- if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
11062
- (0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
13496
+ if (!(0, import_fs10.existsSync)(CONFIG_DIR)) {
13497
+ (0, import_fs10.mkdirSync)(CONFIG_DIR, { recursive: true });
11063
13498
  }
11064
13499
  try {
11065
13500
  const yaml4 = require("js-yaml");
@@ -11071,7 +13506,7 @@ function initDeployConfig() {
11071
13506
  lineWidth: -1,
11072
13507
  noRefs: true
11073
13508
  });
11074
- (0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
13509
+ (0, import_fs10.writeFileSync)(CONFIG_FILE, content, "utf-8");
11075
13510
  const actionLabel = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
11076
13511
  console.log(`\u2705 Server \u90E8\u7F72\u914D\u7F6E${actionLabel}\uFF1A${CONFIG_FILE}`);
11077
13512
  console.log("");
@@ -11299,6 +13734,9 @@ async function main() {
11299
13734
  case "doctor":
11300
13735
  await runDoctorCli();
11301
13736
  break;
13737
+ case "eval":
13738
+ await runOfflineEvaluationCli();
13739
+ break;
11302
13740
  case "init":
11303
13741
  initConfig2();
11304
13742
  break;
@@ -11347,14 +13785,14 @@ async function main() {
11347
13785
  process.exit(command ? 1 : 0);
11348
13786
  }
11349
13787
  }
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;
13788
+ 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
13789
  var init_cli = __esm({
11352
13790
  "src/cli.ts"() {
11353
13791
  import_child_process3 = require("child_process");
11354
13792
  import_crypto4 = require("crypto");
11355
- import_path8 = require("path");
13793
+ import_path9 = require("path");
11356
13794
  import_openurl = __toESM(require("openurl"));
11357
- import_fs9 = require("fs");
13795
+ import_fs10 = require("fs");
11358
13796
  init_index();
11359
13797
  init_processCheck();
11360
13798
  init_constants();
@@ -11364,7 +13802,8 @@ var init_cli = __esm({
11364
13802
  init_doctor();
11365
13803
  init_api_keys();
11366
13804
  init_config();
11367
- PACKAGE_JSON_PATH = (0, import_path8.join)(__dirname, "..", "package.json");
13805
+ init_task_evaluation();
13806
+ PACKAGE_JSON_PATH = (0, import_path9.join)(__dirname, "..", "package.json");
11368
13807
  PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
11369
13808
  PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
11370
13809
  PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";