diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index d02dbd7063..44b672c032 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -1619,6 +1619,19 @@ export default function ProviderDetailPage() { } }, [providerId, isSearchProvider]); + const fetchProxyConfig = useCallback(async () => { + try { + const res = await fetch("/api/settings/proxy", { cache: "no-store" }); + if (res.ok) { + setProxyConfig(await res.json()); + } else { + setProxyConfig(null); + } + } catch { + // Proxy indicators are best-effort. + } + }, []); + const fetchConnections = useCallback(async () => { try { const [connectionsRes, nodesRes] = await Promise.all([ @@ -1680,11 +1693,8 @@ export default function ProviderDetailPage() { fetchConnections(); fetchAliases(); // Load proxy config for visual indicators (provider-level button) - fetch("/api/settings/proxy") - .then((r) => (r.ok ? r.json() : null)) - .then((c) => setProxyConfig(c)) - .catch(() => {}); - }, [fetchConnections, fetchAliases]); + void fetchProxyConfig(); + }, [fetchConnections, fetchAliases, fetchProxyConfig]); const handleZedImport = useCallback(async () => { if (importingZed) return; @@ -4895,7 +4905,10 @@ export default function ProviderDetailPage() { level={proxyTarget.level} levelId={proxyTarget.id} levelLabel={proxyTarget.label} - onSaved={() => void loadConnProxies(connections)} + onSaved={() => { + void fetchProxyConfig(); + void loadConnProxies(connections); + }} /> )} {/* Import Progress Modal */} diff --git a/src/app/api/settings/proxy/route.ts b/src/app/api/settings/proxy/route.ts index 59b64aec69..af1298302f 100755 --- a/src/app/api/settings/proxy/route.ts +++ b/src/app/api/settings/proxy/route.ts @@ -160,6 +160,30 @@ export async function GET(request: Request) { return Response.json({ level, id, proxy }); } + if (level === "provider" && id) { + const assignments = await getProxyAssignments({ scope: "provider" }); + const assignment = assignments.find((entry) => entry.scopeId === id); + if (assignment?.proxyId) { + const proxyData = await getProxyById(assignment.proxyId, { includeSecrets: true }); + if (proxyData) { + return Response.json({ + level, + id, + proxy: { + type: proxyData.type, + host: proxyData.host, + port: proxyData.port, + username: proxyData.username, + password: proxyData.password, + }, + }); + } + } + + const proxy = await getProxyForLevel(level, id); + return Response.json({ level, id, proxy }); + } + if (level) { const proxy = await getProxyForLevel(level, id); return Response.json({ level, id, proxy }); @@ -167,6 +191,24 @@ export async function GET(request: Request) { // Get full config const config = await getProxyConfig(); + const providerAssignments = await getProxyAssignments({ scope: "provider" }); + if (providerAssignments.length > 0) { + config.providers = { ...(config.providers || {}) }; + for (const assignment of providerAssignments) { + if (!assignment.scopeId || !assignment.proxyId) { + continue; + } + const proxyData = await getProxyById(assignment.proxyId, { includeSecrets: true }); + if (!proxyData) continue; + config.providers[assignment.scopeId] = { + type: proxyData.type, + host: proxyData.host, + port: proxyData.port, + username: proxyData.username, + password: proxyData.password, + }; + } + } return Response.json(config); } catch (error) { return createErrorResponseFromUnknown(error, "Failed to load proxy config"); diff --git a/tests/unit/route-edge-coverage.test.ts b/tests/unit/route-edge-coverage.test.ts index bf9a6ce5d9..2292506606 100644 --- a/tests/unit/route-edge-coverage.test.ts +++ b/tests/unit/route-edge-coverage.test.ts @@ -291,6 +291,22 @@ test("settings proxy route covers full config, resolve, validation, delete and g const fullConfig = await settingsProxyRoute.GET( new Request("http://localhost/api/settings/proxy") ); + + const registryProviderProxy = await localDb.createProxy({ + name: "Registry Provider Proxy", + type: "http", + host: "registry-provider.local", + port: 8080, + source: "dashboard-custom", + }); + await localDb.assignProxyToScope("provider", "anthropic", registryProviderProxy.id); + const registryProviderGet = await settingsProxyRoute.GET( + new Request("http://localhost/api/settings/proxy?level=provider&id=anthropic") + ); + const fullConfigWithRegistryProvider = await settingsProxyRoute.GET( + new Request("http://localhost/api/settings/proxy") + ); + const deleted = await settingsProxyRoute.DELETE( new Request("http://localhost/api/settings/proxy?level=provider&id=openai", { method: "DELETE", @@ -310,6 +326,8 @@ test("settings proxy route covers full config, resolve, validation, delete and g const providerGetBody = (await providerGet.json()) as any; const resolveBody = (await resolveGet.json()) as any; const fullConfigBody = (await fullConfig.json()) as any; + const registryProviderGetBody = (await registryProviderGet.json()) as any; + const fullConfigWithRegistryProviderBody = (await fullConfigWithRegistryProvider.json()) as any; const deletedBody = (await deleted.json()) as any; const resolveAfterDeleteBody = (await resolveAfterDelete.json()) as any; const missingLevelBody = (await missingLevel.json()) as any; @@ -329,6 +347,13 @@ test("settings proxy route covers full config, resolve, validation, delete and g assert.equal(resolveBody.proxy.host, "provider.local"); assert.equal(fullConfig.status, 200); assert.equal(fullConfigBody.global.host, "global.local"); + assert.equal(registryProviderGet.status, 200); + assert.equal(registryProviderGetBody.proxy.host, "registry-provider.local"); + assert.equal(fullConfigWithRegistryProvider.status, 200); + assert.equal( + fullConfigWithRegistryProviderBody.providers.anthropic.host, + "registry-provider.local" + ); assert.equal(deleted.status, 200); assert.equal(Object.prototype.hasOwnProperty.call(deletedBody.providers, "openai"), false); assert.equal(resolveAfterDelete.status, 200);