show ai gateway resource details in launcher

This commit is contained in:
miloschwartz
2026-08-11 14:41:07 -04:00
parent e0a66e79bb
commit 98f5e39a7f
18 changed files with 1069 additions and 124 deletions
+14
View File
@@ -590,6 +590,20 @@ authenticated.get(
launcher.listLauncherResources
);
authenticated.get(
"/org/:orgId/launcher/resource/:resourceId/ai-models",
verifyOrgAccess,
verifyResourceAccess,
launcher.listLauncherPublicAiModels
);
authenticated.get(
"/org/:orgId/launcher/site-resource/:siteResourceId/ai-models",
verifyOrgAccess,
verifySiteResourceAccess,
launcher.listLauncherSiteAiModels
);
authenticated.get(
"/org/:orgId/launcher/sites",
verifyOrgAccess,
+13 -10
View File
@@ -98,7 +98,7 @@ function formatTcpUdpResourceAccess(
export function formatPublicResourceAccess(
resource: PublicResourceAccessInput
): LauncherAccessFields {
const browserModes = ["http", "ssh", "rdp", "vnc"];
const browserModes = ["http", "ssh", "rdp", "vnc", "inference"];
if (!browserModes.includes(resource.mode)) {
return formatTcpUdpResourceAccess(
resource.exitNodeEndpoint,
@@ -125,15 +125,10 @@ export function formatPublicResourceAccess(
export function formatSiteResourceAccess(
resource: SiteResourceAccessInput
): LauncherAccessFields {
if (resource.alias) {
return {
accessDisplay: resource.alias,
accessCopyValue: resource.alias,
accessUrl: null
};
}
if (resource.mode === "http" && resource.fullDomain) {
if (
(resource.mode === "http" || resource.mode === "inference") &&
resource.fullDomain
) {
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
return {
accessDisplay: url,
@@ -142,6 +137,14 @@ export function formatSiteResourceAccess(
};
}
if (resource.alias) {
return {
accessDisplay: resource.alias,
accessCopyValue: resource.alias,
accessUrl: null
};
}
const destination = formatSiteResourceDestinationDisplay({
mode: resource.mode as SiteResourceDestinationInput["mode"],
destination: resource.destination,
+5
View File
@@ -5,6 +5,11 @@ export { listLauncherResources } from "./listLauncherResources";
export { listLauncherSites } from "./listLauncherSites";
export { listLauncherLabels } from "./listLauncherLabels";
export { listLauncherViews } from "./listLauncherViews";
export {
listLauncherPublicAiModels,
listLauncherSiteAiModels
} from "./listLauncherAiModels";
export type { ListLauncherAiModelsResponse } from "./listLauncherAiModels";
export { createLauncherView } from "./createLauncherView";
export { updateLauncherView } from "./updateLauncherView";
export { deleteLauncherView } from "./deleteLauncherView";
+102 -41
View File
@@ -31,6 +31,7 @@ import {
inArray,
isNull,
like,
ne,
or,
sql,
type SQL
@@ -40,6 +41,7 @@ import {
formatSiteResourceAccess
} from "./formatLauncherAccess";
import {
LAUNCHER_AI_GATEWAY_GROUP_KEY,
LAUNCHER_FLAT_GROUP_KEY,
LAUNCHER_NO_SITE_GROUP_KEY,
LAUNCHER_UNLABELED_GROUP_KEY,
@@ -652,6 +654,7 @@ async function listSiteGroups(
}
}
let aiGatewayCount = 0;
let noSiteCount = 0;
if (accessible.resourceIds.length > 0 && siteFilterIds.length === 0) {
@@ -665,27 +668,49 @@ async function listSiteGroups(
noSitePublicConditions.push(searchPublic);
}
let noSitePublicQuery = db
.select({
itemCount: countDistinct(resources.resourceId)
})
.from(resources)
.leftJoin(targets, eq(targets.resourceId, resources.resourceId));
const buildNoSitePublicQuery = () => {
let queryBuilder = db
.select({
itemCount: countDistinct(resources.resourceId)
})
.from(resources)
.leftJoin(
targets,
eq(targets.resourceId, resources.resourceId)
);
if (labelFilterIds.length > 0) {
queryBuilder = queryBuilder.innerJoin(
resourceLabels,
eq(resourceLabels.resourceId, resources.resourceId)
);
}
return queryBuilder;
};
if (labelFilterIds.length > 0) {
noSitePublicQuery = noSitePublicQuery.innerJoin(
resourceLabels,
eq(resourceLabels.resourceId, resources.resourceId)
);
noSitePublicConditions.push(
inArray(resourceLabels.labelId, labelFilterIds)
);
}
const [noSitePublicRow] = await noSitePublicQuery.where(
and(...noSitePublicConditions, isNull(targets.targetId))
const [aiGatewayPublicRow] = await buildNoSitePublicQuery().where(
and(
...noSitePublicConditions,
isNull(targets.targetId),
eq(resources.mode, "inference")
)
);
const [noSitePublicRow] = await buildNoSitePublicQuery().where(
and(
...noSitePublicConditions,
isNull(targets.targetId),
ne(resources.mode, "inference")
)
);
aiGatewayCount += Number(aiGatewayPublicRow?.itemCount ?? 0);
noSiteCount += Number(noSitePublicRow?.itemCount ?? 0);
}
@@ -700,38 +725,57 @@ async function listSiteGroups(
noSiteSiteConditions.push(searchSite);
}
let noSiteSiteQuery = db
.select({
itemCount: countDistinct(siteResources.siteResourceId)
})
.from(siteResources)
.leftJoin(
siteNetworks,
eq(siteResources.networkId, siteNetworks.networkId)
)
.leftJoin(sites, eq(siteNetworks.siteId, sites.siteId));
const buildNoSiteSiteQuery = () => {
let queryBuilder = db
.select({
itemCount: countDistinct(siteResources.siteResourceId)
})
.from(siteResources)
.leftJoin(
siteNetworks,
eq(siteResources.networkId, siteNetworks.networkId)
)
.leftJoin(sites, eq(siteNetworks.siteId, sites.siteId));
if (labelFilterIds.length > 0) {
queryBuilder = queryBuilder.innerJoin(
siteResourceLabels,
eq(
siteResourceLabels.siteResourceId,
siteResources.siteResourceId
)
);
}
return queryBuilder;
};
if (labelFilterIds.length > 0) {
noSiteSiteQuery = noSiteSiteQuery.innerJoin(
siteResourceLabels,
eq(
siteResourceLabels.siteResourceId,
siteResources.siteResourceId
)
);
noSiteSiteConditions.push(
inArray(siteResourceLabels.labelId, labelFilterIds)
);
}
const [noSiteSiteRow] = await noSiteSiteQuery.where(
and(...noSiteSiteConditions, isNull(sites.siteId))
const [aiGatewaySiteRow] = await buildNoSiteSiteQuery().where(
and(
...noSiteSiteConditions,
isNull(sites.siteId),
eq(siteResources.mode, "inference")
)
);
const [noSiteSiteRow] = await buildNoSiteSiteQuery().where(
and(
...noSiteSiteConditions,
isNull(sites.siteId),
ne(siteResources.mode, "inference")
)
);
aiGatewayCount += Number(aiGatewaySiteRow?.itemCount ?? 0);
noSiteCount += Number(noSiteSiteRow?.itemCount ?? 0);
}
let groups: LauncherGroup[] = Array.from(siteCountMap.values()).map(
const siteGroups: LauncherGroup[] = Array.from(siteCountMap.values()).map(
(row) => ({
groupKey: String(row.siteId),
name: row.name,
@@ -742,8 +786,26 @@ async function listSiteGroups(
})
);
siteGroups.sort((a, b) => {
const cmp = a.name.localeCompare(b.name, undefined, {
sensitivity: "base"
});
return query.order === "desc" ? -cmp : cmp;
});
const pinnedGroups: LauncherGroup[] = [];
if (aiGatewayCount > 0 && siteFilterIds.length === 0) {
pinnedGroups.push({
groupKey: LAUNCHER_AI_GATEWAY_GROUP_KEY,
name: "AI Gateway",
groupType: "site",
itemCount: aiGatewayCount
});
}
if (noSiteCount > 0 && siteFilterIds.length === 0) {
groups.push({
pinnedGroups.push({
groupKey: LAUNCHER_NO_SITE_GROUP_KEY,
name: "No Site",
groupType: "site",
@@ -751,12 +813,7 @@ async function listSiteGroups(
});
}
groups.sort((a, b) => {
const cmp = a.name.localeCompare(b.name, undefined, {
sensitivity: "base"
});
return query.order === "desc" ? -cmp : cmp;
});
const groups = [...pinnedGroups, ...siteGroups];
const total = groups.length;
return {
@@ -1189,8 +1246,11 @@ function filterResourcesBySite(
items: LauncherResource[],
groupKey: string
): LauncherResource[] {
if (groupKey === LAUNCHER_AI_GATEWAY_GROUP_KEY) {
return items.filter((item) => item.mode === "inference");
}
if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) {
return items.filter((item) => !item.site);
return items.filter((item) => !item.site && item.mode !== "inference");
}
const siteId = Number.parseInt(groupKey, 10);
if (!Number.isFinite(siteId)) {
@@ -1327,7 +1387,8 @@ async function listLauncherResourcesForUserUncached(
const parsedSiteId =
query.groupBy === "site" &&
query.groupKey !== LAUNCHER_NO_SITE_GROUP_KEY
query.groupKey !== LAUNCHER_NO_SITE_GROUP_KEY &&
query.groupKey !== LAUNCHER_AI_GATEWAY_GROUP_KEY
? Number.parseInt(query.groupKey, 10)
: Number.NaN;
const siteIdFilter = Number.isFinite(parsedSiteId)
@@ -0,0 +1,172 @@
import { db, resources, siteResources } from "@server/db";
import { listEffectiveAllowModels } from "@server/lib/aiInferenceResource";
import { response } from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { fromZodError } from "zod-validation-error";
import { z } from "zod";
const publicParamsSchema = z.strictObject({
orgId: z.string().min(1),
resourceId: z.coerce.number().int().positive()
});
const siteParamsSchema = z.strictObject({
orgId: z.string().min(1),
siteResourceId: z.coerce.number().int().positive()
});
export type ListLauncherAiModelsResponse = {
models: Awaited<ReturnType<typeof listEffectiveAllowModels>>;
};
export async function listLauncherPublicAiModels(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = req.userOrgId;
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
const parsed = publicParamsSchema.safeParse(req.params);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromZodError(parsed.error)
)
);
}
const { resourceId } = parsed.data;
const [resource] = await db
.select({
resourceId: resources.resourceId,
mode: resources.mode
})
.from(resources)
.where(
and(
eq(resources.resourceId, resourceId),
eq(resources.orgId, orgId)
)
)
.limit(1);
if (!resource || resource.mode !== "inference") {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"AI models are only available for inference resources"
)
);
}
const models = await listEffectiveAllowModels({ resourceId });
return response<ListLauncherAiModelsResponse>(res, {
data: { models },
success: true,
error: false,
message: "Launcher AI models retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error listing launcher AI models:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}
export async function listLauncherSiteAiModels(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = req.userOrgId;
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
const parsed = siteParamsSchema.safeParse(req.params);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromZodError(parsed.error)
)
);
}
const { siteResourceId } = parsed.data;
const siteResource =
req.siteResource ??
(
await db
.select({
siteResourceId: siteResources.siteResourceId,
mode: siteResources.mode,
orgId: siteResources.orgId
})
.from(siteResources)
.where(
and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.orgId, orgId)
)
)
.limit(1)
)[0];
if (
!siteResource ||
siteResource.orgId !== orgId ||
siteResource.mode !== "inference"
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"AI models are only available for inference resources"
)
);
}
const models = await listEffectiveAllowModels({ siteResourceId });
return response<ListLauncherAiModelsResponse>(res, {
data: { models },
success: true,
error: false,
message: "Launcher AI models retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error listing launcher AI models:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}
+1
View File
@@ -2,6 +2,7 @@ import { z } from "zod";
export const LAUNCHER_UNLABELED_GROUP_KEY = "unlabeled";
export const LAUNCHER_NO_SITE_GROUP_KEY = "no-site";
export const LAUNCHER_AI_GATEWAY_GROUP_KEY = "ai-gateway";
export const LAUNCHER_FLAT_GROUP_KEY = "__all__";
export const launcherViewConfigSchema = z.object({