omniroute 3.0.0 → 3.0.1
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/app/.next/BUILD_ID +1 -1
- package/app/.next/build-manifest.json +2 -2
- package/app/.next/prerender-manifest.json +3 -3
- package/app/.next/server/app/(dashboard)/dashboard/a2a/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/agents/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/analytics/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/api-manager/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/audit-log/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/auto-combo/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/costs/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/health/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/limits/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/logs/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/mcp/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/media/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/onboarding/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/playground/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/search-tools/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/settings/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/400/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/401/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/403/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/408/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/429/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/500/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/502/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/503/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/_global-error.html +2 -2
- package/app/.next/server/app/_global-error.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/docs/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/forbidden/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/forgot-password/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/maintenance/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/offline/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/status/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/terms/page_client-reference-manifest.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__051203a6._.js +2 -2
- package/app/.next/server/chunks/[root-of-the-server]__0891af92._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__1f2b0d89._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__61d78f9d._.js +2 -2
- package/app/.next/server/chunks/[root-of-the-server]__6e52619e._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__afddb4ce._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__e27a89bd._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__fd2fbc93._.js +1 -1
- package/app/.next/server/chunks/_05c48915._.js +1 -1
- package/app/.next/server/chunks/_06515a8a._.js +1 -1
- package/app/.next/server/chunks/_2115d8de._.js +1 -1
- package/app/.next/server/chunks/_3ac953eb._.js +1 -1
- package/app/.next/server/chunks/_4b8fd853._.js +1 -1
- package/app/.next/server/chunks/_68683848._.js +1 -1
- package/app/.next/server/chunks/_6f1b3c3f._.js +1 -1
- package/app/.next/server/chunks/_ee9b677b._.js +1 -1
- package/app/.next/server/chunks/_efd5ede2._.js +2 -2
- package/app/.next/server/chunks/_ffda39da._.js +1 -1
- package/app/.next/server/chunks/open-sse_translator_index_ts_f5fd0821._.js +1 -1
- package/app/.next/server/chunks/src_6320c728._.js +1 -1
- package/app/.next/server/chunks/ssr/[root-of-the-server]__9ef96d20._.js +1 -1
- package/app/.next/server/chunks/ssr/[root-of-the-server]__a6942102._.js +1 -1
- package/app/.next/server/chunks/ssr/_f3674909._.js +1 -1
- package/app/.next/server/chunks/ssr/src_a82a42f9._.js +1 -1
- package/app/.next/server/chunks/ssr/src_app_(dashboard)_dashboard_936a9ee0._.js +1 -1
- package/app/.next/server/chunks/ssr/src_app_(dashboard)_dashboard_settings_9e20fb8d._.js +1 -1
- package/app/.next/server/pages/500.html +2 -2
- package/app/.next/server/server-reference-manifest.js +1 -1
- package/app/.next/server/server-reference-manifest.json +1 -1
- package/app/.next/static/chunks/26aeb12cf79339b1.js +1 -0
- package/app/.next/static/chunks/{80658e0c2b2c7004.js → 633a312ddcf9fa11.js} +1 -1
- package/app/.next/static/chunks/7df0b3c357097db5.js +1 -0
- package/app/.next/static/chunks/a051245b46459ad2.css +1 -0
- package/app/.next/static/chunks/d188e358e1ec0a7d.js +1 -0
- package/app/.next/static/chunks/{4e3fe685e3218d24.js → fa9e2a946d2cdbc7.js} +1 -1
- package/app/CHANGELOG.md +122 -68
- package/app/docs/i18n/ru/README.md +3 -3
- package/app/docs/openapi.yaml +1 -1
- package/app/open-sse/handlers/chatCore.ts +9 -2
- package/app/open-sse/handlers/responseTranslator.ts +13 -3
- package/app/open-sse/services/usage.ts +22 -9
- package/app/open-sse/translator/request/openai-to-claude.ts +10 -2
- package/app/package-lock.json +2 -2
- package/app/package.json +1 -1
- package/app/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +198 -63
- package/app/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx +123 -19
- package/app/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +22 -24
- package/app/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +5 -0
- package/app/src/lib/providers/validation.ts +21 -0
- package/app/src/shared/components/ProxyConfigModal.tsx +42 -13
- package/package.json +1 -1
- package/app/.next/static/chunks/594bdf69c19b5310.js +0 -1
- package/app/.next/static/chunks/643428fcd3f9bb51.js +0 -1
- package/app/.next/static/chunks/8efe14530c87d9f6.css +0 -1
- package/app/.next/static/chunks/d0cff7a6c3a66c7d.js +0 -1
- /package/app/.next/static/{cQZnxkFFvb8O7T4x7OSsG → bF7pUIj1e9VsHgSW5x9WX}/_buildManifest.js +0 -0
- /package/app/.next/static/{cQZnxkFFvb8O7T4x7OSsG → bF7pUIj1e9VsHgSW5x9WX}/_clientMiddlewareManifest.json +0 -0
- /package/app/.next/static/{cQZnxkFFvb8O7T4x7OSsG → bF7pUIj1e9VsHgSW5x9WX}/_ssgManifest.js +0 -0
|
@@ -802,6 +802,9 @@ export default function ProviderDetailPage() {
|
|
|
802
802
|
const userDismissed = useRef(false);
|
|
803
803
|
const [proxyTarget, setProxyTarget] = useState(null);
|
|
804
804
|
const [proxyConfig, setProxyConfig] = useState(null);
|
|
805
|
+
const [connProxyMap, setConnProxyMap] = useState<
|
|
806
|
+
Record<string, { proxy: any; level: string } | null>
|
|
807
|
+
>({});
|
|
805
808
|
const [importingModels, setImportingModels] = useState(false);
|
|
806
809
|
const [showImportModal, setShowImportModal] = useState(false);
|
|
807
810
|
const [importProgress, setImportProgress] = useState({
|
|
@@ -938,18 +941,48 @@ export default function ProviderDetailPage() {
|
|
|
938
941
|
useEffect(() => {
|
|
939
942
|
fetchConnections();
|
|
940
943
|
fetchAliases();
|
|
941
|
-
// Load proxy config for visual indicators
|
|
944
|
+
// Load proxy config for visual indicators (provider-level button)
|
|
942
945
|
fetch("/api/settings/proxy")
|
|
943
946
|
.then((r) => (r.ok ? r.json() : null))
|
|
944
947
|
.then((c) => setProxyConfig(c))
|
|
945
948
|
.catch(() => {});
|
|
946
949
|
}, [fetchConnections, fetchAliases]);
|
|
947
950
|
|
|
951
|
+
const loadConnProxies = useCallback(async (conns: { id?: string }[]) => {
|
|
952
|
+
if (!conns.length) return;
|
|
953
|
+
try {
|
|
954
|
+
const results = await Promise.all(
|
|
955
|
+
conns
|
|
956
|
+
.filter((c) => c.id)
|
|
957
|
+
.map((c) =>
|
|
958
|
+
fetch(`/api/settings/proxy?resolve=${encodeURIComponent(c.id!)}`, { cache: "no-store" })
|
|
959
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
960
|
+
.then((data) => [c.id!, data] as [string, any])
|
|
961
|
+
.catch(() => [c.id!, null] as [string, any])
|
|
962
|
+
)
|
|
963
|
+
);
|
|
964
|
+
const map: Record<string, { proxy: any; level: string } | null> = {};
|
|
965
|
+
for (const [id, data] of results) {
|
|
966
|
+
map[id] = data?.proxy ? data : null;
|
|
967
|
+
}
|
|
968
|
+
setConnProxyMap(map);
|
|
969
|
+
} catch {
|
|
970
|
+
// ignore
|
|
971
|
+
}
|
|
972
|
+
}, []);
|
|
973
|
+
|
|
948
974
|
useEffect(() => {
|
|
949
975
|
if (loading || isSearchProvider) return;
|
|
950
976
|
fetchProviderModelMeta();
|
|
951
977
|
}, [loading, isSearchProvider, fetchProviderModelMeta]);
|
|
952
978
|
|
|
979
|
+
// Load per-connection effective proxy (handles registry assignments)
|
|
980
|
+
useEffect(() => {
|
|
981
|
+
if (!loading && connections.length > 0) {
|
|
982
|
+
void loadConnProxies(connections);
|
|
983
|
+
}
|
|
984
|
+
}, [loading, connections, loadConnProxies]);
|
|
985
|
+
|
|
953
986
|
// Auto-open Add Connection modal when no connections exist (better UX)
|
|
954
987
|
// Only fires once on initial load, not on HMR remounts or after user dismissal
|
|
955
988
|
useEffect(() => {
|
|
@@ -1930,68 +1963,153 @@ export default function ProviderDetailPage() {
|
|
|
1930
1963
|
)}
|
|
1931
1964
|
</div>
|
|
1932
1965
|
) : (
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1966
|
+
(() => {
|
|
1967
|
+
// Group connections by tag (providerSpecificData.tag)
|
|
1968
|
+
const sorted = [...connections].sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
1969
|
+
const hasAnyTag = sorted.some((c) => c.providerSpecificData?.tag as string | undefined);
|
|
1970
|
+
|
|
1971
|
+
if (!hasAnyTag) {
|
|
1972
|
+
// No tags — render flat list as before
|
|
1973
|
+
return (
|
|
1974
|
+
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
|
1975
|
+
{sorted.map((conn, index) => (
|
|
1976
|
+
<ConnectionRow
|
|
1977
|
+
key={conn.id}
|
|
1978
|
+
connection={conn}
|
|
1979
|
+
isOAuth={isOAuth}
|
|
1980
|
+
isFirst={index === 0}
|
|
1981
|
+
isLast={index === sorted.length - 1}
|
|
1982
|
+
onMoveUp={() => handleSwapPriority(conn, sorted[index - 1])}
|
|
1983
|
+
onMoveDown={() => handleSwapPriority(conn, sorted[index + 1])}
|
|
1984
|
+
onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)}
|
|
1985
|
+
onToggleRateLimit={(enabled) => handleToggleRateLimit(conn.id, enabled)}
|
|
1986
|
+
isCodex={providerId === "codex"}
|
|
1987
|
+
onToggleCodex5h={(enabled) =>
|
|
1988
|
+
handleToggleCodexLimit(conn.id, "use5h", enabled)
|
|
1989
|
+
}
|
|
1990
|
+
onToggleCodexWeekly={(enabled) =>
|
|
1991
|
+
handleToggleCodexLimit(conn.id, "useWeekly", enabled)
|
|
1992
|
+
}
|
|
1993
|
+
onRetest={() => handleRetestConnection(conn.id)}
|
|
1994
|
+
isRetesting={retestingId === conn.id}
|
|
1995
|
+
onEdit={() => {
|
|
1996
|
+
setSelectedConnection(conn);
|
|
1997
|
+
setShowEditModal(true);
|
|
1998
|
+
}}
|
|
1999
|
+
onDelete={() => handleDelete(conn.id)}
|
|
2000
|
+
onReauth={isOAuth ? () => setShowOAuthModal(true) : undefined}
|
|
2001
|
+
onRefreshToken={isOAuth ? () => handleRefreshToken(conn.id) : undefined}
|
|
2002
|
+
isRefreshing={refreshingId === conn.id}
|
|
2003
|
+
onProxy={() =>
|
|
2004
|
+
setProxyTarget({
|
|
2005
|
+
level: "key",
|
|
2006
|
+
id: conn.id,
|
|
2007
|
+
label: conn.name || conn.email || conn.id,
|
|
2008
|
+
})
|
|
2009
|
+
}
|
|
2010
|
+
hasProxy={!!connProxyMap[conn.id]?.proxy}
|
|
2011
|
+
proxySource={connProxyMap[conn.id]?.level || null}
|
|
2012
|
+
proxyHost={connProxyMap[conn.id]?.proxy?.host || null}
|
|
2013
|
+
/>
|
|
2014
|
+
))}
|
|
2015
|
+
</div>
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Build ordered tag groups: untagged first, then alphabetically
|
|
2020
|
+
const groupMap = new Map<string, typeof sorted>();
|
|
2021
|
+
for (const conn of sorted) {
|
|
2022
|
+
const tag = (conn.providerSpecificData?.tag as string | undefined)?.trim() || "";
|
|
2023
|
+
if (!groupMap.has(tag)) groupMap.set(tag, []);
|
|
2024
|
+
groupMap.get(tag)!.push(conn);
|
|
2025
|
+
}
|
|
2026
|
+
const groupKeys = Array.from(groupMap.keys()).sort((a, b) => {
|
|
2027
|
+
if (a === "") return -1;
|
|
2028
|
+
if (b === "") return 1;
|
|
2029
|
+
return a.localeCompare(b);
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
return (
|
|
2033
|
+
<div className="flex flex-col gap-0">
|
|
2034
|
+
{groupKeys.map((tag, gi) => {
|
|
2035
|
+
const groupConns = groupMap.get(tag)!;
|
|
2036
|
+
return (
|
|
2037
|
+
<div
|
|
2038
|
+
key={tag || "__untagged__"}
|
|
2039
|
+
className={
|
|
2040
|
+
gi > 0
|
|
2041
|
+
? "border-t border-black/[0.06] dark:border-white/[0.06] mt-1 pt-1"
|
|
2042
|
+
: ""
|
|
2043
|
+
}
|
|
2044
|
+
>
|
|
2045
|
+
{tag && (
|
|
2046
|
+
<div className="flex items-center gap-2 px-3 pt-2 pb-1">
|
|
2047
|
+
<span className="material-symbols-outlined text-[13px] text-text-muted/50">
|
|
2048
|
+
label
|
|
2049
|
+
</span>
|
|
2050
|
+
<span className="text-[11px] font-semibold uppercase tracking-widest text-text-muted/60 select-none">
|
|
2051
|
+
{tag}
|
|
2052
|
+
</span>
|
|
2053
|
+
<div className="flex-1 h-px bg-black/[0.04] dark:bg-white/[0.04]" />
|
|
2054
|
+
<span className="text-[10px] text-text-muted/40">
|
|
2055
|
+
{groupConns.length}
|
|
2056
|
+
</span>
|
|
2057
|
+
</div>
|
|
2058
|
+
)}
|
|
2059
|
+
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
|
2060
|
+
{groupConns.map((conn, index) => (
|
|
2061
|
+
<ConnectionRow
|
|
2062
|
+
key={conn.id}
|
|
2063
|
+
connection={conn}
|
|
2064
|
+
isOAuth={isOAuth}
|
|
2065
|
+
isFirst={gi === 0 && index === 0}
|
|
2066
|
+
isLast={gi === groupKeys.length - 1 && index === groupConns.length - 1}
|
|
2067
|
+
onMoveUp={() =>
|
|
2068
|
+
handleSwapPriority(conn, sorted[sorted.indexOf(conn) - 1])
|
|
2069
|
+
}
|
|
2070
|
+
onMoveDown={() =>
|
|
2071
|
+
handleSwapPriority(conn, sorted[sorted.indexOf(conn) + 1])
|
|
2072
|
+
}
|
|
2073
|
+
onToggleActive={(isActive) =>
|
|
2074
|
+
handleUpdateConnectionStatus(conn.id, isActive)
|
|
2075
|
+
}
|
|
2076
|
+
onToggleRateLimit={(enabled) => handleToggleRateLimit(conn.id, enabled)}
|
|
2077
|
+
isCodex={providerId === "codex"}
|
|
2078
|
+
onToggleCodex5h={(enabled) =>
|
|
2079
|
+
handleToggleCodexLimit(conn.id, "use5h", enabled)
|
|
2080
|
+
}
|
|
2081
|
+
onToggleCodexWeekly={(enabled) =>
|
|
2082
|
+
handleToggleCodexLimit(conn.id, "useWeekly", enabled)
|
|
2083
|
+
}
|
|
2084
|
+
onRetest={() => handleRetestConnection(conn.id)}
|
|
2085
|
+
isRetesting={retestingId === conn.id}
|
|
2086
|
+
onEdit={() => {
|
|
2087
|
+
setSelectedConnection(conn);
|
|
2088
|
+
setShowEditModal(true);
|
|
2089
|
+
}}
|
|
2090
|
+
onDelete={() => handleDelete(conn.id)}
|
|
2091
|
+
onReauth={isOAuth ? () => setShowOAuthModal(true) : undefined}
|
|
2092
|
+
onRefreshToken={isOAuth ? () => handleRefreshToken(conn.id) : undefined}
|
|
2093
|
+
isRefreshing={refreshingId === conn.id}
|
|
2094
|
+
onProxy={() =>
|
|
2095
|
+
setProxyTarget({
|
|
2096
|
+
level: "key",
|
|
2097
|
+
id: conn.id,
|
|
2098
|
+
label: conn.name || conn.email || conn.id,
|
|
2099
|
+
})
|
|
2100
|
+
}
|
|
2101
|
+
hasProxy={!!connProxyMap[conn.id]?.proxy}
|
|
2102
|
+
proxySource={connProxyMap[conn.id]?.level || null}
|
|
2103
|
+
proxyHost={connProxyMap[conn.id]?.proxy?.host || null}
|
|
2104
|
+
/>
|
|
2105
|
+
))}
|
|
2106
|
+
</div>
|
|
2107
|
+
</div>
|
|
2108
|
+
);
|
|
2109
|
+
})}
|
|
2110
|
+
</div>
|
|
2111
|
+
);
|
|
2112
|
+
})()
|
|
1995
2113
|
)}
|
|
1996
2114
|
</Card>
|
|
1997
2115
|
|
|
@@ -2188,6 +2306,7 @@ export default function ProviderDetailPage() {
|
|
|
2188
2306
|
level={proxyTarget.level}
|
|
2189
2307
|
levelId={proxyTarget.id}
|
|
2190
2308
|
levelLabel={proxyTarget.label}
|
|
2309
|
+
onSaved={() => void loadConnProxies(connections)}
|
|
2191
2310
|
/>
|
|
2192
2311
|
)}
|
|
2193
2312
|
{/* Import Progress Modal */}
|
|
@@ -4130,6 +4249,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
|
|
4130
4249
|
baseUrl: "",
|
|
4131
4250
|
region: "",
|
|
4132
4251
|
validationModelId: "",
|
|
4252
|
+
tag: "",
|
|
4133
4253
|
});
|
|
4134
4254
|
const [testing, setTesting] = useState(false);
|
|
4135
4255
|
const [testResult, setTestResult] = useState(null);
|
|
@@ -4159,6 +4279,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
|
|
4159
4279
|
baseUrl: existingBaseUrl || (isBailian ? defaultBailianUrl : ""),
|
|
4160
4280
|
region: existingRegion || (isVertex ? defaultRegion : ""),
|
|
4161
4281
|
validationModelId: (connection.providerSpecificData?.validationModelId as string) || "",
|
|
4282
|
+
tag: (connection.providerSpecificData?.tag as string) || "",
|
|
4162
4283
|
});
|
|
4163
4284
|
// Load existing extra keys from providerSpecificData
|
|
4164
4285
|
const existing = connection.providerSpecificData?.extraApiKeys;
|
|
@@ -4282,6 +4403,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
|
|
4282
4403
|
updates.providerSpecificData = {
|
|
4283
4404
|
...(connection.providerSpecificData || {}),
|
|
4284
4405
|
extraApiKeys: extraApiKeys.filter((k) => k.trim().length > 0),
|
|
4406
|
+
tag: formData.tag.trim() || undefined,
|
|
4285
4407
|
};
|
|
4286
4408
|
if (formData.validationModelId) {
|
|
4287
4409
|
updates.providerSpecificData.validationModelId = formData.validationModelId;
|
|
@@ -4292,6 +4414,12 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
|
|
4292
4414
|
} else if (isVertex) {
|
|
4293
4415
|
updates.providerSpecificData.region = formData.region;
|
|
4294
4416
|
}
|
|
4417
|
+
} else {
|
|
4418
|
+
// Also persist tag for OAuth accounts
|
|
4419
|
+
updates.providerSpecificData = {
|
|
4420
|
+
...(connection.providerSpecificData || {}),
|
|
4421
|
+
tag: formData.tag.trim() || undefined,
|
|
4422
|
+
};
|
|
4295
4423
|
}
|
|
4296
4424
|
const error = (await onSave(updates)) as void | unknown;
|
|
4297
4425
|
if (error) {
|
|
@@ -4322,6 +4450,13 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
|
|
4322
4450
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
4323
4451
|
placeholder={isOAuth ? t("accountName") : t("productionKey")}
|
|
4324
4452
|
/>
|
|
4453
|
+
<Input
|
|
4454
|
+
label="Tag / Group"
|
|
4455
|
+
value={formData.tag}
|
|
4456
|
+
onChange={(e) => setFormData({ ...formData, tag: e.target.value })}
|
|
4457
|
+
placeholder="e.g. personal, work, team-a"
|
|
4458
|
+
hint="Used to group accounts in the provider view"
|
|
4459
|
+
/>
|
|
4325
4460
|
{isOAuth && connection.email && (
|
|
4326
4461
|
<div className="bg-sidebar/50 p-3 rounded-lg">
|
|
4327
4462
|
<p className="text-sm text-text-muted mb-1">{t("email")}</p>
|
|
@@ -27,6 +27,14 @@ type HealthInfo = {
|
|
|
27
27
|
lastSeenAt: string | null;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
type TestResult = {
|
|
31
|
+
success: boolean;
|
|
32
|
+
publicIp?: string;
|
|
33
|
+
latencyMs?: number;
|
|
34
|
+
country?: string;
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
30
38
|
const EMPTY_FORM = {
|
|
31
39
|
id: "",
|
|
32
40
|
name: "",
|
|
@@ -51,6 +59,8 @@ export default function ProxyRegistryManager() {
|
|
|
51
59
|
|
|
52
60
|
const [usageById, setUsageById] = useState<Record<string, UsageInfo>>({});
|
|
53
61
|
const [healthById, setHealthById] = useState<Record<string, HealthInfo>>({});
|
|
62
|
+
const [testById, setTestById] = useState<Record<string, TestResult | null>>({});
|
|
63
|
+
const [testingId, setTestingId] = useState<string | null>(null);
|
|
54
64
|
const [migrating, setMigrating] = useState(false);
|
|
55
65
|
const [bulkOpen, setBulkOpen] = useState(false);
|
|
56
66
|
const [bulkSaving, setBulkSaving] = useState(false);
|
|
@@ -75,6 +85,36 @@ export default function ProxyRegistryManager() {
|
|
|
75
85
|
}
|
|
76
86
|
}, []);
|
|
77
87
|
|
|
88
|
+
const loadAllUsage = useCallback(async (proxyIds: string[]) => {
|
|
89
|
+
if (!proxyIds.length) return;
|
|
90
|
+
try {
|
|
91
|
+
const results = await Promise.all(
|
|
92
|
+
proxyIds.map((id) =>
|
|
93
|
+
fetch(`/api/settings/proxies/assignments?proxyId=${encodeURIComponent(id)}`)
|
|
94
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
95
|
+
.then((data) => {
|
|
96
|
+
const rawAssignments: Array<{ scope: string; scopeId: string | null }> =
|
|
97
|
+
Array.isArray(data?.items) ? data.items : [];
|
|
98
|
+
// Deduplicate by scope+scopeId — prevents double-counting when both
|
|
99
|
+
// a provider-scope and account-scope row exist for the same proxy
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
const assignments = rawAssignments.filter((a) => {
|
|
102
|
+
const key = `${a.scope}:${a.scopeId ?? ""}`;
|
|
103
|
+
if (seen.has(key)) return false;
|
|
104
|
+
seen.add(key);
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
return [id, { count: assignments.length, assignments }] as [string, UsageInfo];
|
|
108
|
+
})
|
|
109
|
+
.catch(() => [id, { count: 0, assignments: [] }] as [string, UsageInfo])
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
setUsageById(Object.fromEntries(results));
|
|
113
|
+
} catch {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
78
118
|
const load = useCallback(async () => {
|
|
79
119
|
setLoading(true);
|
|
80
120
|
setError(null);
|
|
@@ -86,15 +126,18 @@ export default function ProxyRegistryManager() {
|
|
|
86
126
|
setItems([]);
|
|
87
127
|
return;
|
|
88
128
|
}
|
|
89
|
-
|
|
129
|
+
const loaded: ProxyItem[] = Array.isArray(data?.items) ? data.items : [];
|
|
130
|
+
setItems(loaded);
|
|
131
|
+
const ids = loaded.map((p) => p.id).filter(Boolean);
|
|
90
132
|
void loadHealth();
|
|
133
|
+
void loadAllUsage(ids);
|
|
91
134
|
} catch (e: any) {
|
|
92
135
|
setError(e?.message || "Failed to load proxy registry");
|
|
93
136
|
setItems([]);
|
|
94
137
|
} finally {
|
|
95
138
|
setLoading(false);
|
|
96
139
|
}
|
|
97
|
-
}, [loadHealth]);
|
|
140
|
+
}, [loadHealth, loadAllUsage]);
|
|
98
141
|
|
|
99
142
|
useEffect(() => {
|
|
100
143
|
void load();
|
|
@@ -130,22 +173,63 @@ export default function ProxyRegistryManager() {
|
|
|
130
173
|
const loadUsage = async (proxyId: string) => {
|
|
131
174
|
try {
|
|
132
175
|
const res = await fetch(
|
|
133
|
-
`/api/settings/proxies?
|
|
176
|
+
`/api/settings/proxies/assignments?proxyId=${encodeURIComponent(proxyId)}`
|
|
134
177
|
);
|
|
135
178
|
const data = await res.json().catch(() => ({}));
|
|
136
179
|
if (!res.ok) return;
|
|
180
|
+
const rawAssignments: Array<{ scope: string; scopeId: string | null }> = Array.isArray(
|
|
181
|
+
data?.items
|
|
182
|
+
)
|
|
183
|
+
? data.items
|
|
184
|
+
: [];
|
|
185
|
+
const seen = new Set<string>();
|
|
186
|
+
const assignments = rawAssignments.filter((a) => {
|
|
187
|
+
const key = `${a.scope}:${a.scopeId ?? ""}`;
|
|
188
|
+
if (seen.has(key)) return false;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
137
192
|
setUsageById((prev) => ({
|
|
138
193
|
...prev,
|
|
139
|
-
[proxyId]: {
|
|
140
|
-
count: Number(data?.count || 0),
|
|
141
|
-
assignments: Array.isArray(data?.assignments) ? data.assignments : [],
|
|
142
|
-
},
|
|
194
|
+
[proxyId]: { count: assignments.length, assignments },
|
|
143
195
|
}));
|
|
144
196
|
} catch {
|
|
145
197
|
// ignore usage loading errors in UI
|
|
146
198
|
}
|
|
147
199
|
};
|
|
148
200
|
|
|
201
|
+
const handleTestProxy = async (item: ProxyItem) => {
|
|
202
|
+
if (testingId) return;
|
|
203
|
+
setTestingId(item.id);
|
|
204
|
+
setTestById((prev) => ({ ...prev, [item.id]: null }));
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch("/api/settings/proxy/test", {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
proxy: {
|
|
211
|
+
type: item.type || "http",
|
|
212
|
+
host: item.host,
|
|
213
|
+
port: String(item.port || 8080),
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
const data = await res.json().catch(() => ({}));
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
setTestById((prev) => ({
|
|
220
|
+
...prev,
|
|
221
|
+
[item.id]: { success: false, error: data?.error?.message || "Test failed" },
|
|
222
|
+
}));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
setTestById((prev) => ({ ...prev, [item.id]: { success: true, ...data } }));
|
|
226
|
+
} catch (e: any) {
|
|
227
|
+
setTestById((prev) => ({ ...prev, [item.id]: { success: false, error: e?.message } }));
|
|
228
|
+
} finally {
|
|
229
|
+
setTestingId(null);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
149
233
|
const handleSave = async () => {
|
|
150
234
|
if (!form.name.trim() || !form.host.trim()) {
|
|
151
235
|
setError("Name and host are required");
|
|
@@ -378,27 +462,47 @@ export default function ProxyRegistryManager() {
|
|
|
378
462
|
</span>
|
|
379
463
|
</td>
|
|
380
464
|
<td className="py-2 pr-3 text-xs text-text-muted">
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
465
|
+
<div className="flex flex-col gap-0.5">
|
|
466
|
+
{health ? (
|
|
467
|
+
<>
|
|
468
|
+
<span>{health.successRate ?? 0}% success</span>
|
|
469
|
+
<span>{health.avgLatencyMs ?? "-"} ms avg</span>
|
|
470
|
+
</>
|
|
471
|
+
) : testById[item.id] ? (
|
|
472
|
+
testById[item.id]!.success ? (
|
|
473
|
+
<>
|
|
474
|
+
<span className="text-emerald-400">
|
|
475
|
+
✓ {testById[item.id]!.publicIp}
|
|
476
|
+
</span>
|
|
477
|
+
{testById[item.id]!.latencyMs && (
|
|
478
|
+
<span>{testById[item.id]!.latencyMs}ms</span>
|
|
479
|
+
)}
|
|
480
|
+
</>
|
|
481
|
+
) : (
|
|
482
|
+
<span className="text-red-400">
|
|
483
|
+
{testById[item.id]!.error || "failed"}
|
|
484
|
+
</span>
|
|
485
|
+
)
|
|
486
|
+
) : (
|
|
487
|
+
<span>—</span>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
389
490
|
</td>
|
|
390
491
|
<td className="py-2 pr-3 text-xs text-text-muted">
|
|
391
|
-
{
|
|
492
|
+
{usageById[item.id] != null
|
|
493
|
+
? `${usageById[item.id].count} assignment(s)`
|
|
494
|
+
: "—"}
|
|
392
495
|
</td>
|
|
393
496
|
<td className="py-2">
|
|
394
497
|
<div className="flex items-center gap-1">
|
|
395
498
|
<Button
|
|
396
499
|
size="sm"
|
|
397
500
|
variant="ghost"
|
|
398
|
-
icon="
|
|
399
|
-
onClick={() => void
|
|
501
|
+
icon="speed"
|
|
502
|
+
onClick={() => void handleTestProxy(item)}
|
|
503
|
+
loading={testingId === item.id}
|
|
400
504
|
>
|
|
401
|
-
|
|
505
|
+
Test
|
|
402
506
|
</Button>
|
|
403
507
|
<Button
|
|
404
508
|
size="sm"
|