diff --git a/messages/en-US.json b/messages/en-US.json index b6e24adcc..0c59990a7 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1891,6 +1891,7 @@ "aiProviderRoutingModeTargetDescription": "Route through targets on your sites", "aiProviderRoutingModeTargetNote": "After creating this provider, configure site targets on the Network Settings tab.", "aiProviderTargetNoOne": "This provider doesn't have any targets. Add a target to route requests through your sites.", + "aiProviderRemoteNodeTargetsWarning": "Sites connected to remote nodes are inaccessable to be routed to on AI Gateway providers.", "aiProviderSkipTlsVerification": "Skip TLS Verification", "aiProviderSkipTlsVerificationDescription": "Disable TLS certificate verification for the upstream connection", "aiProviderBudget": "Budget", diff --git a/server/routers/aiGateway/targetRouting.ts b/server/routers/aiGateway/targetRouting.ts index 963eac2d5..2dd8c721d 100644 --- a/server/routers/aiGateway/targetRouting.ts +++ b/server/routers/aiGateway/targetRouting.ts @@ -99,6 +99,7 @@ async function fetchProviderTargets( method: targets.method, exitNodeSubnet: sites.exitNodeSubnet, reachableAt: exitNodes.reachableAt, + exitNodeType: exitNodes.type, hcHealth: targetHealthCheck.hcHealth }) .from(targets) @@ -119,6 +120,12 @@ async function fetchProviderTargets( if (!row.exitNodeSubnet || !row.reachableAt) { continue; } + // Sites connected to a remote exit node aren't reachable via a + // gerbil sidecar's /router/* proxy - only "gerbil" type exit nodes + // run that endpoint. + if (row.exitNodeType !== "gerbil") { + continue; + } // A target with an active health check that's currently failing is // taken out of rotation. No health check (null) or "unknown" (check // hasn't run yet / hcEnabled is off) still routes normally, matching diff --git a/src/app/[orgId]/settings/ai-providers/[niceId]/network/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/network/page.tsx index fafbdf49f..bcf75b7f7 100644 --- a/src/app/[orgId]/settings/ai-providers/[niceId]/network/page.tsx +++ b/src/app/[orgId]/settings/ai-providers/[niceId]/network/page.tsx @@ -345,6 +345,7 @@ export default function AiProviderNetworkPage() { ref={targetsFormRef} orgId={orgId} isHttp + isAiProvider providerId={provider.providerId} initialTargets={ isTargetModeSaved ? remoteTargets : [] diff --git a/src/app/[orgId]/settings/ai-providers/create/page.tsx b/src/app/[orgId]/settings/ai-providers/create/page.tsx index 5872594ef..f4609b1cc 100644 --- a/src/app/[orgId]/settings/ai-providers/create/page.tsx +++ b/src/app/[orgId]/settings/ai-providers/create/page.tsx @@ -682,6 +682,7 @@ export default function CreateAiProviderPage() { { targetsRef.current = nextTargets; }} diff --git a/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx b/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx index 2cc33acaf..ba331ed4e 100644 --- a/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx +++ b/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx @@ -113,6 +113,8 @@ type ProxyResourceTargetsFormProps = { hideSaveButton?: boolean; /** Hide the advanced mode toggle and always use non-advanced mode (e.g. AI providers) */ disableAdvancedMode?: boolean; + /** Targets picker is for an AI provider (changes which routing warnings are shown) */ + isAiProvider?: boolean; }; export const ProxyResourceTargetsForm = forwardRef< @@ -131,7 +133,8 @@ export const ProxyResourceTargetsForm = forwardRef< emptyMessage, embedded = false, hideSaveButton = false, - disableAdvancedMode = false + disableAdvancedMode = false, + isAiProvider = false }, ref ) { @@ -259,6 +262,14 @@ export const ProxyResourceTargetsForm = forwardRef< }) ); + const { data: remoteExitNodes = [] } = useQuery({ + ...orgQueries.remoteExitNodes({ orgId }), + enabled: build === "saas" && isAiProvider + }); + const hasRemoteExitNodes = remoteExitNodes.some( + (node) => node.exitNodeId !== null + ); + const updateTarget = useCallback( (targetId: number, data: Partial) => { setTargets((prevTargets) => { @@ -972,6 +983,7 @@ export const ProxyResourceTargetsForm = forwardRef< )} {build === "saas" && + !isAiProvider && targets.length > 1 && new Set(targets.map((t) => t.siteId)).size > 1 && (

@@ -988,6 +1000,11 @@ export const ProxyResourceTargetsForm = forwardRef< .

)} + {build === "saas" && isAiProvider && hasRemoteExitNodes && ( +

+ {t("aiProviderRemoteNodeTargetsWarning")} +

+ )} ); diff --git a/src/lib/queries.ts b/src/lib/queries.ts index ce3fe960c..8d3831dbd 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -59,6 +59,7 @@ import type { import type { GetResourceResponse } from "@server/routers/resource/getResource"; import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo"; import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; +import type { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import type { ListRolesResponse } from "@server/routers/role"; import type { ListSitesResponse } from "@server/routers/site"; import type { @@ -330,6 +331,17 @@ export const orgQueries = { } }), + remoteExitNodes: ({ orgId }: { orgId: string }) => + queryOptions({ + queryKey: ["ORG", orgId, "REMOTE_EXIT_NODES"] as const, + queryFn: async ({ signal, meta }) => { + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/remote-exit-nodes`, { signal }); + return res.data.data.remoteExitNodes; + } + }), + labels: ({ orgId, query,