@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/README.md +108 -4
- package/config/trigger.advanced.yaml +11 -0
- package/config/trigger.routing.yaml +67 -0
- package/dist/cli.js +2608 -169
- package/dist/cli.js.map +4 -4
- package/docs/configuration-guide.md +33 -8
- package/docs/release-notes-v1.2.0.md +43 -0
- package/docs/release-notes-v1.3.0.md +39 -0
- package/docs/releasing.md +2 -0
- package/docs/remote-client-guide.md +11 -12
- package/docs/server-maintainer-guide.md +2 -2
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
235
|
+
const normalizedScopes = normalizeKnownScopes(scopes);
|
|
236
|
+
if (normalizedScopes.includes("admin")) {
|
|
230
237
|
return true;
|
|
231
238
|
}
|
|
232
239
|
if (required === "read-only") {
|
|
233
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 :
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2707
|
+
await (0, import_promises2.writeFile)(filePath, (0, import_zlib.gzipSync)(Buffer.from(content, "utf-8")));
|
|
2325
2708
|
return;
|
|
2326
2709
|
}
|
|
2327
|
-
(0,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
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
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
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
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
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
|
-
|
|
3623
|
-
|
|
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
|
-
|
|
3626
|
-
|
|
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:
|
|
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
|
|
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=>({ '&':'&','<':'<','>':'>','"':'"' }[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=>({ '&':'&','<':'<','>':'>','"':'"' }[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: "
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
5514
|
-
const cliPath =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
5866
|
-
await (0,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
7804
|
-
if (!(0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
7858
|
-
log(
|
|
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
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
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
|
|
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
|
-
|
|
8207
|
-
|
|
9528
|
+
import_fs7 = require("fs");
|
|
9529
|
+
import_promises5 = require("fs/promises");
|
|
8208
9530
|
import_os2 = require("os");
|
|
8209
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 ||
|
|
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,
|
|
9263
|
-
(0,
|
|
9264
|
-
(0,
|
|
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,
|
|
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
|
|
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
|
-
|
|
11298
|
+
import_fs8 = require("fs");
|
|
9975
11299
|
import_crypto3 = require("crypto");
|
|
9976
11300
|
import_net2 = require("net");
|
|
9977
11301
|
import_os3 = require("os");
|
|
9978
|
-
|
|
9979
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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\
|
|
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
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
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
|
|
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
|
-
|
|
10699
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
10769
|
-
const content = (0,
|
|
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,
|
|
10773
|
-
const content = (0,
|
|
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,
|
|
10777
|
-
const content = (0,
|
|
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,
|
|
13174
|
+
if (!(0, import_fs10.existsSync)(configFile)) {
|
|
10845
13175
|
continue;
|
|
10846
13176
|
}
|
|
10847
|
-
const content = (0,
|
|
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(
|
|
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,
|
|
11006
|
-
(0,
|
|
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,
|
|
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(
|
|
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,
|
|
11062
|
-
(0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
13793
|
+
import_path9 = require("path");
|
|
11356
13794
|
import_openurl = __toESM(require("openurl"));
|
|
11357
|
-
|
|
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
|
-
|
|
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/";
|