@khanglvm/llm-router 2.0.0-beta.1 → 2.0.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/CHANGELOG.md +27 -0
- package/README.md +163 -426
- package/package.json +3 -3
- package/src/cli/router-module.js +2773 -2587
- package/src/cli-entry.js +32 -103
- package/src/node/activity-log.js +119 -0
- package/src/node/coding-tool-config.js +85 -11
- package/src/node/config-workflows.js +51 -12
- package/src/node/instance-state.js +1 -1
- package/src/node/litellm-context-catalog.js +184 -0
- package/src/node/local-server.js +23 -3
- package/src/node/port-reclaim.js +2 -2
- package/src/node/start-command.js +22 -22
- package/src/node/startup-manager.js +3 -3
- package/src/node/web-command.js +1 -1
- package/src/node/web-console-assets.js +1 -1
- package/src/node/web-console-client.js +34 -29
- package/src/node/web-console-server.js +420 -38
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/buffered-text-input.js +133 -0
- package/src/node/web-console-ui/config-editor-utils.js +57 -4
- package/src/node/web-console-ui/dropdown-placement.js +153 -0
- package/src/node/web-console-ui/select-search-utils.js +6 -0
- package/src/node/web-console-ui/transient-integer-input-utils.js +12 -0
- package/src/runtime/balancer.js +78 -1
- package/src/runtime/codex-request-transformer.js +16 -7
- package/src/runtime/config.js +448 -12
- package/src/runtime/handler/amp-response.js +5 -3
- package/src/runtime/handler/amp-web-search.js +2232 -0
- package/src/runtime/handler/fallback.js +30 -2
- package/src/runtime/handler/provider-call.js +353 -36
- package/src/runtime/handler/provider-translation.js +14 -0
- package/src/runtime/handler/request.js +128 -2
- package/src/runtime/handler/route-debug.js +36 -0
- package/src/runtime/handler.js +210 -20
- package/src/runtime/subscription-provider.js +1 -1
- package/src/shared/coding-tool-bindings.js +49 -0
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/request/claude-to-openai.js +43 -0
package/src/runtime/config.js
CHANGED
|
@@ -181,12 +181,18 @@ function normalizeAmpModelMappingEntry(entry) {
|
|
|
181
181
|
entry.ref ??
|
|
182
182
|
""
|
|
183
183
|
).trim();
|
|
184
|
+
const sourceRouteKey = normalizeAmpRouteKey(
|
|
185
|
+
entry.sourceRouteKey ??
|
|
186
|
+
entry["source-route-key"] ??
|
|
187
|
+
""
|
|
188
|
+
);
|
|
184
189
|
|
|
185
|
-
if (!from || !to) return null;
|
|
190
|
+
if (!from || (!to && !sourceRouteKey)) return null;
|
|
186
191
|
|
|
187
192
|
return {
|
|
188
193
|
from,
|
|
189
|
-
to
|
|
194
|
+
...(to ? { to } : {}),
|
|
195
|
+
...(sourceRouteKey ? { sourceRouteKey } : {})
|
|
190
196
|
};
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -473,6 +479,289 @@ function normalizeAmpSubagentDefinitions(rawDefinitions) {
|
|
|
473
479
|
return out;
|
|
474
480
|
}
|
|
475
481
|
|
|
482
|
+
const AMP_WEB_SEARCH_PROVIDER_IDS = Object.freeze([
|
|
483
|
+
"brave",
|
|
484
|
+
"tavily",
|
|
485
|
+
"exa",
|
|
486
|
+
"searxng"
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
const AMP_WEB_SEARCH_PROVIDER_DEFAULT_LIMITS = Object.freeze({
|
|
490
|
+
brave: 1000,
|
|
491
|
+
tavily: 1000,
|
|
492
|
+
exa: 1000,
|
|
493
|
+
searxng: 0
|
|
494
|
+
});
|
|
495
|
+
const AMP_WEB_SEARCH_DEFAULT_COUNT = 5;
|
|
496
|
+
const AMP_WEB_SEARCH_MIN_COUNT = 1;
|
|
497
|
+
const AMP_WEB_SEARCH_MAX_COUNT = 20;
|
|
498
|
+
|
|
499
|
+
function looksLikeHostedWebSearchRouteId(value) {
|
|
500
|
+
const text = String(value || "").trim();
|
|
501
|
+
return text.includes("/") && parseRouteReference(text).type === "direct";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function buildHostedWebSearchRouteId(providerId, modelId) {
|
|
505
|
+
const normalizedProviderId = String(providerId || "").trim();
|
|
506
|
+
const normalizedModelId = String(modelId || "").trim();
|
|
507
|
+
if (!normalizedProviderId || !normalizedModelId) return "";
|
|
508
|
+
return `${normalizedProviderId}/${normalizedModelId}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function normalizeHostedWebSearchProviderRoute(entry, explicitId = "") {
|
|
512
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
513
|
+
|
|
514
|
+
const explicitRouteId = String(explicitId || "").trim();
|
|
515
|
+
const providerId = String(
|
|
516
|
+
entry.providerId
|
|
517
|
+
?? entry.provider
|
|
518
|
+
?? entry.routeProviderId
|
|
519
|
+
?? entry["provider-id"]
|
|
520
|
+
?? ""
|
|
521
|
+
).trim();
|
|
522
|
+
const modelId = String(
|
|
523
|
+
entry.model
|
|
524
|
+
?? entry.modelId
|
|
525
|
+
?? entry.routeModelId
|
|
526
|
+
?? entry["model-id"]
|
|
527
|
+
?? ""
|
|
528
|
+
).trim();
|
|
529
|
+
const routeId = String(
|
|
530
|
+
explicitRouteId
|
|
531
|
+
|| entry.id
|
|
532
|
+
|| buildHostedWebSearchRouteId(providerId, modelId)
|
|
533
|
+
).trim();
|
|
534
|
+
if (!looksLikeHostedWebSearchRouteId(routeId)) return null;
|
|
535
|
+
|
|
536
|
+
const parsed = parseRouteReference(routeId);
|
|
537
|
+
if (parsed.type !== "direct") return null;
|
|
538
|
+
const normalizedProviderId = providerId || parsed.providerId;
|
|
539
|
+
const normalizedModelId = modelId || parsed.modelId;
|
|
540
|
+
const normalizedRouteId = buildHostedWebSearchRouteId(normalizedProviderId, normalizedModelId);
|
|
541
|
+
if (!normalizedRouteId || normalizedRouteId !== routeId) return null;
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
id: normalizedRouteId,
|
|
545
|
+
providerId: normalizedProviderId,
|
|
546
|
+
model: normalizedModelId
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function normalizeAmpWebSearchProviderId(value) {
|
|
551
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
552
|
+
return AMP_WEB_SEARCH_PROVIDER_IDS.includes(normalized) ? normalized : "";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function normalizeAmpWebSearchStrategy(value) {
|
|
556
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
557
|
+
if (!normalized) return "ordered";
|
|
558
|
+
if (normalized === "quota-balance" || normalized === "quota-balanced") return "quota-balance";
|
|
559
|
+
if (normalized === "quota-aware-weighted-rr") return "quota-balance";
|
|
560
|
+
return "ordered";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function parseNonNegativeInteger(value, fallback = 0) {
|
|
564
|
+
const parsed = Number(value);
|
|
565
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
566
|
+
return Math.floor(parsed);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function parseAmpWebSearchCount(value, fallback = AMP_WEB_SEARCH_DEFAULT_COUNT) {
|
|
570
|
+
const parsed = Number(value);
|
|
571
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
572
|
+
return Math.min(AMP_WEB_SEARCH_MAX_COUNT, Math.max(AMP_WEB_SEARCH_MIN_COUNT, Math.floor(parsed)));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function hasOwnWebSearchProviderField(entry, keys = []) {
|
|
576
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
577
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(entry, key) && entry[key] !== undefined && entry[key] !== null && String(entry[key]).trim() !== "");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function normalizeAmpWebSearchProviderEntry(entry, explicitId = "", { preserveUnconfigured = true, inheritedCount = AMP_WEB_SEARCH_DEFAULT_COUNT } = {}) {
|
|
581
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
582
|
+
|
|
583
|
+
const hostedRoute = normalizeHostedWebSearchProviderRoute(entry, explicitId);
|
|
584
|
+
if (hostedRoute) return hostedRoute;
|
|
585
|
+
|
|
586
|
+
const id = normalizeAmpWebSearchProviderId(
|
|
587
|
+
explicitId
|
|
588
|
+
|| entry.id
|
|
589
|
+
|| entry.provider
|
|
590
|
+
|| entry.backend
|
|
591
|
+
|| entry.name
|
|
592
|
+
);
|
|
593
|
+
if (!id) return null;
|
|
594
|
+
|
|
595
|
+
const apiKey = String(
|
|
596
|
+
entry.apiKey
|
|
597
|
+
?? entry["api-key"]
|
|
598
|
+
?? entry.key
|
|
599
|
+
?? ""
|
|
600
|
+
).trim();
|
|
601
|
+
const url = sanitizeEndpointUrl(
|
|
602
|
+
entry.url
|
|
603
|
+
?? entry.baseUrl
|
|
604
|
+
?? entry["base-url"]
|
|
605
|
+
?? entry.searxngUrl
|
|
606
|
+
?? entry["searxng-url"]
|
|
607
|
+
?? ""
|
|
608
|
+
).replace(/\/+$/, "");
|
|
609
|
+
const hasExplicitCount = hasOwnWebSearchProviderField(entry, ["count", "resultCount", "result-count", "resultsPerCall", "results-per-call"]);
|
|
610
|
+
const hasExplicitLimit = hasOwnWebSearchProviderField(entry, ["limit", "monthlyLimit", "monthly-limit", "quota"]);
|
|
611
|
+
const hasExplicitRemaining = hasOwnWebSearchProviderField(entry, ["remaining", "remainingQuota", "remaining-quota", "remainingQueries", "remaining-queries"]);
|
|
612
|
+
const hasCredential = id === "searxng" ? Boolean(url) : Boolean(apiKey);
|
|
613
|
+
if (!preserveUnconfigured && !hasCredential && !hasExplicitCount && !hasExplicitLimit && !hasExplicitRemaining) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const count = parseAmpWebSearchCount(
|
|
618
|
+
entry.count
|
|
619
|
+
?? entry.resultCount
|
|
620
|
+
?? entry["result-count"]
|
|
621
|
+
?? entry.resultsPerCall
|
|
622
|
+
?? entry["results-per-call"],
|
|
623
|
+
inheritedCount
|
|
624
|
+
);
|
|
625
|
+
const includeQuotaDefaults = hasCredential || hasExplicitLimit || hasExplicitRemaining;
|
|
626
|
+
const limitFallback = includeQuotaDefaults ? (AMP_WEB_SEARCH_PROVIDER_DEFAULT_LIMITS[id] || 0) : 0;
|
|
627
|
+
const limit = parseNonNegativeInteger(
|
|
628
|
+
entry.limit
|
|
629
|
+
?? entry.monthlyLimit
|
|
630
|
+
?? entry["monthly-limit"]
|
|
631
|
+
?? entry.quota
|
|
632
|
+
?? limitFallback,
|
|
633
|
+
limitFallback
|
|
634
|
+
);
|
|
635
|
+
const remainingFallback = limit > 0 ? limit : 0;
|
|
636
|
+
const remaining = parseNonNegativeInteger(
|
|
637
|
+
entry.remaining
|
|
638
|
+
?? entry.remainingQuota
|
|
639
|
+
?? entry["remaining-quota"]
|
|
640
|
+
?? entry.remainingQueries
|
|
641
|
+
?? entry["remaining-queries"]
|
|
642
|
+
?? remainingFallback,
|
|
643
|
+
remainingFallback
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
const normalizedEntry = {
|
|
647
|
+
id,
|
|
648
|
+
...(id === "searxng"
|
|
649
|
+
? (url ? { url } : {})
|
|
650
|
+
: (apiKey ? { apiKey } : {})),
|
|
651
|
+
...(count !== AMP_WEB_SEARCH_DEFAULT_COUNT ? { count } : {}),
|
|
652
|
+
...(hasExplicitLimit || (includeQuotaDefaults && limit > 0) ? { limit } : {}),
|
|
653
|
+
...(hasExplicitRemaining || (includeQuotaDefaults && (limit > 0 || remaining > 0))
|
|
654
|
+
? { remaining: limit > 0 ? Math.min(remaining, limit) : remaining }
|
|
655
|
+
: {})
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
return normalizedEntry;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function normalizeAmpWebSearchProviders(rawProviders, rawWebSearch = {}) {
|
|
662
|
+
const inheritedCount = parseAmpWebSearchCount(rawWebSearch.count, AMP_WEB_SEARCH_DEFAULT_COUNT);
|
|
663
|
+
if (Array.isArray(rawProviders)) {
|
|
664
|
+
const ordered = [];
|
|
665
|
+
const seen = new Set();
|
|
666
|
+
for (const entry of rawProviders) {
|
|
667
|
+
const normalized = normalizeAmpWebSearchProviderEntry(entry, "", {
|
|
668
|
+
preserveUnconfigured: true,
|
|
669
|
+
inheritedCount
|
|
670
|
+
});
|
|
671
|
+
if (!normalized || seen.has(normalized.id)) continue;
|
|
672
|
+
seen.add(normalized.id);
|
|
673
|
+
ordered.push(normalized);
|
|
674
|
+
}
|
|
675
|
+
return ordered;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (rawProviders && typeof rawProviders === "object") {
|
|
679
|
+
const ordered = [];
|
|
680
|
+
const seen = new Set();
|
|
681
|
+
for (const [providerId, entry] of Object.entries(rawProviders)) {
|
|
682
|
+
const normalized = normalizeAmpWebSearchProviderEntry(entry, providerId, {
|
|
683
|
+
preserveUnconfigured: true,
|
|
684
|
+
inheritedCount
|
|
685
|
+
});
|
|
686
|
+
if (!normalized || seen.has(normalized.id)) continue;
|
|
687
|
+
seen.add(normalized.id);
|
|
688
|
+
ordered.push(normalized);
|
|
689
|
+
}
|
|
690
|
+
return ordered;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const legacyPreferredBackend = normalizeAmpWebSearchProviderId(
|
|
694
|
+
rawWebSearch.preferredBackend ?? rawWebSearch["preferred-backend"]
|
|
695
|
+
);
|
|
696
|
+
const legacyEntries = [
|
|
697
|
+
normalizeAmpWebSearchProviderEntry({
|
|
698
|
+
apiKey: rawWebSearch.braveApiKey ?? rawWebSearch["brave-api-key"],
|
|
699
|
+
count: rawWebSearch.count,
|
|
700
|
+
limit: rawWebSearch.braveMonthlyLimit ?? rawWebSearch["brave-monthly-limit"],
|
|
701
|
+
remaining: rawWebSearch.braveRemaining ?? rawWebSearch["brave-remaining"]
|
|
702
|
+
}, "brave", { preserveUnconfigured: false, inheritedCount }),
|
|
703
|
+
normalizeAmpWebSearchProviderEntry({
|
|
704
|
+
apiKey: rawWebSearch.tavilyApiKey ?? rawWebSearch["tavily-api-key"],
|
|
705
|
+
count: rawWebSearch.count,
|
|
706
|
+
limit: rawWebSearch.tavilyMonthlyLimit ?? rawWebSearch["tavily-monthly-limit"],
|
|
707
|
+
remaining: rawWebSearch.tavilyRemaining ?? rawWebSearch["tavily-remaining"]
|
|
708
|
+
}, "tavily", { preserveUnconfigured: false, inheritedCount }),
|
|
709
|
+
normalizeAmpWebSearchProviderEntry({
|
|
710
|
+
apiKey: rawWebSearch.exaApiKey ?? rawWebSearch["exa-api-key"],
|
|
711
|
+
count: rawWebSearch.count,
|
|
712
|
+
limit: rawWebSearch.exaMonthlyLimit ?? rawWebSearch["exa-monthly-limit"],
|
|
713
|
+
remaining: rawWebSearch.exaRemaining ?? rawWebSearch["exa-remaining"]
|
|
714
|
+
}, "exa", { preserveUnconfigured: false, inheritedCount }),
|
|
715
|
+
normalizeAmpWebSearchProviderEntry({
|
|
716
|
+
count: rawWebSearch.count,
|
|
717
|
+
url: rawWebSearch.searxngUrl ?? rawWebSearch["searxng-url"] ?? rawWebSearch.url
|
|
718
|
+
}, "searxng", { preserveUnconfigured: false, inheritedCount })
|
|
719
|
+
].filter(Boolean);
|
|
720
|
+
|
|
721
|
+
if (!legacyPreferredBackend) return legacyEntries;
|
|
722
|
+
|
|
723
|
+
return [
|
|
724
|
+
...legacyEntries.filter((entry) => entry.id === legacyPreferredBackend),
|
|
725
|
+
...legacyEntries.filter((entry) => entry.id !== legacyPreferredBackend)
|
|
726
|
+
];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function normalizeAmpWebSearchConfig(rawWebSearch) {
|
|
730
|
+
if (!rawWebSearch || typeof rawWebSearch !== "object" || Array.isArray(rawWebSearch)) return undefined;
|
|
731
|
+
|
|
732
|
+
const providers = normalizeAmpWebSearchProviders(rawWebSearch.providers, rawWebSearch);
|
|
733
|
+
const count = parseAmpWebSearchCount(rawWebSearch.count, AMP_WEB_SEARCH_DEFAULT_COUNT);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
strategy: normalizeAmpWebSearchStrategy(rawWebSearch.strategy),
|
|
737
|
+
count,
|
|
738
|
+
providers
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function supportsOpenAIHostedWebSearchRoute(provider, model) {
|
|
743
|
+
const providerFormats = dedupeStrings([...(provider?.formats || []), provider?.format]);
|
|
744
|
+
if (!providerFormats.includes(FORMATS.OPENAI)) return false;
|
|
745
|
+
|
|
746
|
+
const modelId = String(model?.id || "").trim();
|
|
747
|
+
const preferredFormat = modelId
|
|
748
|
+
? String(provider?.lastProbe?.modelPreferredFormat?.[modelId] || "").trim()
|
|
749
|
+
: "";
|
|
750
|
+
if (preferredFormat) {
|
|
751
|
+
return preferredFormat === FORMATS.OPENAI;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const probedFormats = modelId
|
|
755
|
+
? dedupeStrings(provider?.lastProbe?.modelSupport?.[modelId] || [])
|
|
756
|
+
: [];
|
|
757
|
+
if (probedFormats.length > 0) {
|
|
758
|
+
return probedFormats.includes(FORMATS.OPENAI);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const modelFormats = dedupeStrings([...(model?.formats || []), model?.format]);
|
|
762
|
+
return modelFormats.length === 0 || modelFormats.includes(FORMATS.OPENAI);
|
|
763
|
+
}
|
|
764
|
+
|
|
476
765
|
function normalizeAmpConfig(rawAmp) {
|
|
477
766
|
const source = rawAmp && typeof rawAmp === "object" && !Array.isArray(rawAmp)
|
|
478
767
|
? rawAmp
|
|
@@ -484,6 +773,33 @@ function normalizeAmpConfig(rawAmp) {
|
|
|
484
773
|
const hasRawModelRoutes = hasOwn(source, "rawModelRoutes") || hasOwn(source, "raw-model-routes");
|
|
485
774
|
const hasOverrides = hasOwn(source, "overrides");
|
|
486
775
|
const hasFallback = hasOwn(source, "fallback");
|
|
776
|
+
const hasWebSearch = hasOwn(source, "webSearch")
|
|
777
|
+
|| hasOwn(source, "web-search")
|
|
778
|
+
|| [
|
|
779
|
+
"preferredBackend",
|
|
780
|
+
"preferred-backend",
|
|
781
|
+
"count",
|
|
782
|
+
"braveApiKey",
|
|
783
|
+
"brave-api-key",
|
|
784
|
+
"tavilyApiKey",
|
|
785
|
+
"tavily-api-key",
|
|
786
|
+
"exaApiKey",
|
|
787
|
+
"exa-api-key",
|
|
788
|
+
"searxngUrl",
|
|
789
|
+
"searxng-url",
|
|
790
|
+
"braveMonthlyLimit",
|
|
791
|
+
"brave-monthly-limit",
|
|
792
|
+
"tavilyMonthlyLimit",
|
|
793
|
+
"tavily-monthly-limit",
|
|
794
|
+
"exaMonthlyLimit",
|
|
795
|
+
"exa-monthly-limit",
|
|
796
|
+
"braveRemaining",
|
|
797
|
+
"brave-remaining",
|
|
798
|
+
"tavilyRemaining",
|
|
799
|
+
"tavily-remaining",
|
|
800
|
+
"exaRemaining",
|
|
801
|
+
"exa-remaining"
|
|
802
|
+
].some((key) => hasOwn(source, key));
|
|
487
803
|
const normalizedSubagentDefinitions = normalizeAmpSubagentDefinitions(
|
|
488
804
|
source.subagentDefinitions ?? source["subagent-definitions"]
|
|
489
805
|
);
|
|
@@ -497,6 +813,7 @@ function normalizeAmpConfig(rawAmp) {
|
|
|
497
813
|
.filter(Boolean);
|
|
498
814
|
const normalizedOverrides = normalizeAmpOverrides(source.overrides);
|
|
499
815
|
const normalizedFallback = normalizeAmpFallback(source.fallback);
|
|
816
|
+
const normalizedWebSearch = normalizeAmpWebSearchConfig(source.webSearch ?? source["web-search"] ?? source);
|
|
500
817
|
|
|
501
818
|
return {
|
|
502
819
|
upstreamUrl: sanitizeEndpointUrl(
|
|
@@ -565,6 +882,11 @@ function normalizeAmpConfig(rawAmp) {
|
|
|
565
882
|
fallback: normalizedFallback
|
|
566
883
|
}
|
|
567
884
|
: {}),
|
|
885
|
+
...(hasWebSearch && normalizedWebSearch !== undefined
|
|
886
|
+
? {
|
|
887
|
+
webSearch: normalizedWebSearch
|
|
888
|
+
}
|
|
889
|
+
: {}),
|
|
568
890
|
...(normalizedSubagentDefinitions !== undefined
|
|
569
891
|
? {
|
|
570
892
|
subagentDefinitions: normalizedSubagentDefinitions
|
|
@@ -1352,7 +1674,21 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
1352
1674
|
: (typeof raw["master-key"] === "string" ? raw["master-key"] : undefined);
|
|
1353
1675
|
|
|
1354
1676
|
const defaultModel = rawDefaultModel;
|
|
1355
|
-
const
|
|
1677
|
+
const rawAmp = raw.amp ?? raw.ampcode ?? raw["amp-code"];
|
|
1678
|
+
const rawAmpObject = rawAmp && typeof rawAmp === "object" && !Array.isArray(rawAmp)
|
|
1679
|
+
? rawAmp
|
|
1680
|
+
: {};
|
|
1681
|
+
const rawWebSearch = raw.webSearch ?? raw["web-search"];
|
|
1682
|
+
const amp = normalizeAmpConfig(rawAmp);
|
|
1683
|
+
const webSearch = normalizeAmpWebSearchConfig(rawWebSearch)
|
|
1684
|
+
?? (amp?.webSearch && typeof amp.webSearch === "object" && !Array.isArray(amp.webSearch)
|
|
1685
|
+
? amp.webSearch
|
|
1686
|
+
: undefined);
|
|
1687
|
+
const normalizedAmp = webSearch && amp?.webSearch && typeof amp.webSearch === "object" && !Array.isArray(amp.webSearch)
|
|
1688
|
+
? Object.fromEntries(
|
|
1689
|
+
Object.entries(amp).filter(([key]) => key !== "webSearch")
|
|
1690
|
+
)
|
|
1691
|
+
: amp;
|
|
1356
1692
|
|
|
1357
1693
|
const normalized = {
|
|
1358
1694
|
version: inferNormalizedConfigVersion(raw, providers, modelAliases),
|
|
@@ -1360,7 +1696,8 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
1360
1696
|
defaultModel,
|
|
1361
1697
|
providers,
|
|
1362
1698
|
modelAliases,
|
|
1363
|
-
amp,
|
|
1699
|
+
amp: normalizedAmp,
|
|
1700
|
+
...(webSearch ? { webSearch } : {}),
|
|
1364
1701
|
metadata: sanitizeRuntimeMetadata(raw.metadata)
|
|
1365
1702
|
};
|
|
1366
1703
|
Object.defineProperty(normalized, NORMALIZATION_ISSUES_SYMBOL, {
|
|
@@ -1601,6 +1938,70 @@ function validateAmpConfig(config, routingIndex, errors) {
|
|
|
1601
1938
|
if (!entry?.route) continue;
|
|
1602
1939
|
validateAmpRouteReference(String(entry.route), routingIndex, errors, `AMP override signature '${entry.id || "(unknown)"}'`);
|
|
1603
1940
|
}
|
|
1941
|
+
|
|
1942
|
+
const webSearch = config?.webSearch ?? amp?.webSearch;
|
|
1943
|
+
if (webSearch && typeof webSearch === "object" && !Array.isArray(webSearch)) {
|
|
1944
|
+
if (!["ordered", "quota-balance"].includes(String(webSearch.strategy || "ordered").trim())) {
|
|
1945
|
+
errors.push(`webSearch has unsupported strategy '${webSearch.strategy}'.`);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
const providers = Array.isArray(webSearch.providers) ? webSearch.providers : [];
|
|
1949
|
+
const seenProviderIds = new Set();
|
|
1950
|
+
for (const provider of providers) {
|
|
1951
|
+
const providerId = String(provider?.id || "").trim();
|
|
1952
|
+
if (!providerId) {
|
|
1953
|
+
errors.push("webSearch provider is missing id.");
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (seenProviderIds.has(providerId)) {
|
|
1957
|
+
errors.push(`webSearch provider '${providerId}' is duplicated.`);
|
|
1958
|
+
} else {
|
|
1959
|
+
seenProviderIds.add(providerId);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
if (AMP_WEB_SEARCH_PROVIDER_IDS.includes(providerId)) {
|
|
1963
|
+
const count = provider?.count;
|
|
1964
|
+
if (count !== undefined && (!Number.isFinite(Number(count)) || Number(count) < AMP_WEB_SEARCH_MIN_COUNT || Number(count) > AMP_WEB_SEARCH_MAX_COUNT)) {
|
|
1965
|
+
errors.push(`webSearch provider '${providerId}' has invalid count '${count}'.`);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
const limit = provider?.limit;
|
|
1969
|
+
if (limit !== undefined && (!Number.isFinite(Number(limit)) || Number(limit) < 0)) {
|
|
1970
|
+
errors.push(`webSearch provider '${providerId}' has invalid limit '${limit}'.`);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const remaining = provider?.remaining;
|
|
1974
|
+
if (remaining !== undefined && (!Number.isFinite(Number(remaining)) || Number(remaining) < 0)) {
|
|
1975
|
+
errors.push(`webSearch provider '${providerId}' has invalid remaining '${remaining}'.`);
|
|
1976
|
+
}
|
|
1977
|
+
if (
|
|
1978
|
+
remaining !== undefined
|
|
1979
|
+
&& Number.isFinite(Number(limit))
|
|
1980
|
+
&& Number(limit) > 0
|
|
1981
|
+
&& Number(remaining) > Number(limit)
|
|
1982
|
+
) {
|
|
1983
|
+
errors.push(`webSearch provider '${providerId}' remaining cannot exceed limit.`);
|
|
1984
|
+
}
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const hostedRoute = normalizeHostedWebSearchProviderRoute(provider, providerId);
|
|
1989
|
+
if (!hostedRoute) {
|
|
1990
|
+
errors.push(`webSearch provider '${providerId}' is unsupported.`);
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const resolvedRoute = resolveRouteReference(config, hostedRoute.id);
|
|
1995
|
+
if (!resolvedRoute?.provider || !resolvedRoute?.model) {
|
|
1996
|
+
errors.push(`webSearch provider '${providerId}' does not reference a configured provider/model route.`);
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (!supportsOpenAIHostedWebSearchRoute(resolvedRoute.provider, resolvedRoute.model)) {
|
|
2001
|
+
errors.push(`webSearch provider '${providerId}' must reference an OpenAI-compatible provider/model route.`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
1604
2005
|
}
|
|
1605
2006
|
|
|
1606
2007
|
export function validateRuntimeConfig(config, { requireMasterKey = false, requireProvider = false } = {}) {
|
|
@@ -1810,15 +2211,38 @@ export function maskSecret(value) {
|
|
|
1810
2211
|
}
|
|
1811
2212
|
|
|
1812
2213
|
export function sanitizeConfigForDisplay(config) {
|
|
2214
|
+
const sanitizedWebSearch = config.webSearch && typeof config.webSearch === "object" && !Array.isArray(config.webSearch)
|
|
2215
|
+
? {
|
|
2216
|
+
...config.webSearch,
|
|
2217
|
+
providers: (Array.isArray(config.webSearch.providers) ? config.webSearch.providers : []).map((provider) => ({
|
|
2218
|
+
...provider,
|
|
2219
|
+
apiKey: provider?.apiKey ? maskSecret(provider.apiKey) : undefined
|
|
2220
|
+
}))
|
|
2221
|
+
}
|
|
2222
|
+
: undefined;
|
|
2223
|
+
const sanitizedAmp = config.amp
|
|
2224
|
+
? {
|
|
2225
|
+
...config.amp,
|
|
2226
|
+
upstreamApiKey: config.amp.upstreamApiKey ? maskSecret(config.amp.upstreamApiKey) : undefined,
|
|
2227
|
+
...(config.amp.webSearch && typeof config.amp.webSearch === "object" && !Array.isArray(config.amp.webSearch)
|
|
2228
|
+
? {
|
|
2229
|
+
webSearch: {
|
|
2230
|
+
...config.amp.webSearch,
|
|
2231
|
+
providers: (Array.isArray(config.amp.webSearch.providers) ? config.amp.webSearch.providers : []).map((provider) => ({
|
|
2232
|
+
...provider,
|
|
2233
|
+
apiKey: provider?.apiKey ? maskSecret(provider.apiKey) : undefined
|
|
2234
|
+
}))
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
: {})
|
|
2238
|
+
}
|
|
2239
|
+
: undefined;
|
|
2240
|
+
|
|
1813
2241
|
return {
|
|
1814
2242
|
...config,
|
|
1815
2243
|
masterKey: config.masterKey ? maskSecret(config.masterKey) : undefined,
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
...config.amp,
|
|
1819
|
-
upstreamApiKey: config.amp.upstreamApiKey ? maskSecret(config.amp.upstreamApiKey) : undefined
|
|
1820
|
-
}
|
|
1821
|
-
: undefined,
|
|
2244
|
+
...(sanitizedWebSearch ? { webSearch: sanitizedWebSearch } : {}),
|
|
2245
|
+
amp: sanitizedAmp,
|
|
1822
2246
|
providers: (config.providers || []).map((provider) => ({
|
|
1823
2247
|
...provider,
|
|
1824
2248
|
apiKey: provider.apiKey ? maskSecret(provider.apiKey) : undefined
|
|
@@ -2528,7 +2952,7 @@ function resolveAmpConfiguredDefinitionTarget(definitions, routes, propertyName
|
|
|
2528
2952
|
};
|
|
2529
2953
|
}
|
|
2530
2954
|
|
|
2531
|
-
function resolveAmpRawModelMappedTarget(config, requestedModel) {
|
|
2955
|
+
function resolveAmpRawModelMappedTarget(config, requestedModel, routingIndex) {
|
|
2532
2956
|
const mappings = Array.isArray(config?.amp?.rawModelRoutes)
|
|
2533
2957
|
? config.amp.rawModelRoutes
|
|
2534
2958
|
: [];
|
|
@@ -2538,6 +2962,18 @@ function resolveAmpRawModelMappedTarget(config, requestedModel) {
|
|
|
2538
2962
|
if (!matchAmpModelPattern(mapping.from, requestedModel)) continue;
|
|
2539
2963
|
const target = String(mapping.to || "").trim();
|
|
2540
2964
|
if (target) return target;
|
|
2965
|
+
|
|
2966
|
+
const sourceRouteKey = String(mapping.sourceRouteKey || "").trim();
|
|
2967
|
+
if (!sourceRouteKey) continue;
|
|
2968
|
+
|
|
2969
|
+
const routeTarget = String(config?.amp?.routes?.[sourceRouteKey] || "").trim();
|
|
2970
|
+
if (routeTarget) return routeTarget;
|
|
2971
|
+
|
|
2972
|
+
const ampDefaultTarget = String(config?.amp?.defaultRoute || "").trim();
|
|
2973
|
+
if (ampDefaultTarget) return ampDefaultTarget;
|
|
2974
|
+
|
|
2975
|
+
const globalDefaultTarget = getDefaultRouteReference(config, routingIndex);
|
|
2976
|
+
if (globalDefaultTarget) return globalDefaultTarget;
|
|
2541
2977
|
}
|
|
2542
2978
|
|
|
2543
2979
|
return "";
|
|
@@ -2660,7 +3096,7 @@ function resolveAmpRequestedRoute(config, effectiveRequested, normalizedRequeste
|
|
|
2660
3096
|
: {};
|
|
2661
3097
|
const entityTarget = resolveAmpConfiguredDefinitionTarget(matchingEntities, routes);
|
|
2662
3098
|
const signatureTarget = resolveAmpConfiguredDefinitionTarget(matchingSignatures, routes);
|
|
2663
|
-
const rawMappedTarget = resolveAmpRawModelMappedTarget(config, effectiveRequested);
|
|
3099
|
+
const rawMappedTarget = resolveAmpRawModelMappedTarget(config, effectiveRequested, routingIndex);
|
|
2664
3100
|
|
|
2665
3101
|
const entityRoute = entityTarget.target
|
|
2666
3102
|
? resolveLocalRoute(entityTarget.target, {
|
|
@@ -12,7 +12,9 @@ function isPlainObject(value) {
|
|
|
12
12
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function readAmpVisibleModel(requestBody) {
|
|
15
|
+
function readAmpVisibleModel(requestBody, env = {}) {
|
|
16
|
+
const overrideModel = String(env?.LLM_ROUTER_DEBUG_AMP_VISIBLE_MODEL_OVERRIDE || "").trim();
|
|
17
|
+
if (overrideModel) return overrideModel;
|
|
16
18
|
return typeof requestBody?.model === "string" ? requestBody.model.trim() : "";
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -270,10 +272,10 @@ function rewriteAmpStreamResponse(response, visibleModel) {
|
|
|
270
272
|
});
|
|
271
273
|
}
|
|
272
274
|
|
|
273
|
-
export async function maybeRewriteAmpClientResponse(response, { clientType, requestBody, stream = false } = {}) {
|
|
275
|
+
export async function maybeRewriteAmpClientResponse(response, { clientType, requestBody, stream = false, env = {} } = {}) {
|
|
274
276
|
if (clientType !== "amp" || !(response instanceof Response)) return response;
|
|
275
277
|
|
|
276
|
-
const visibleModel = readAmpVisibleModel(requestBody);
|
|
278
|
+
const visibleModel = readAmpVisibleModel(requestBody, env);
|
|
277
279
|
if (!visibleModel) return response;
|
|
278
280
|
|
|
279
281
|
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|