mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-13 16:00:02 +02:00
show ai gateway resource details in launcher
This commit is contained in:
@@ -518,6 +518,152 @@ export async function listSiteResourceAiProviders(siteResourceId: number) {
|
||||
.where(eq(siteResourceAiProviders.siteResourceId, siteResourceId));
|
||||
}
|
||||
|
||||
export type EffectiveAllowModel = {
|
||||
modelId: number;
|
||||
modelKey: string;
|
||||
name: string;
|
||||
providerId: number;
|
||||
providerName: string;
|
||||
};
|
||||
|
||||
export async function listEffectiveAllowModels(options: {
|
||||
resourceId?: number;
|
||||
siteResourceId?: number;
|
||||
}): Promise<EffectiveAllowModel[]> {
|
||||
if (
|
||||
options.resourceId === undefined &&
|
||||
options.siteResourceId === undefined
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const attachments =
|
||||
options.resourceId !== undefined
|
||||
? await listPublicResourceAiProviders(options.resourceId)
|
||||
: await listSiteResourceAiProviders(options.siteResourceId!);
|
||||
|
||||
const activeAttachments = attachments.filter(
|
||||
(a) => a.enabled && a.providerEnabled
|
||||
);
|
||||
if (activeAttachments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const inheritProviderIds = activeAttachments
|
||||
.filter((a) => a.accessMode === "inherit")
|
||||
.map((a) => a.providerId);
|
||||
const selectProviderIds = activeAttachments
|
||||
.filter((a) => a.accessMode === "select")
|
||||
.map((a) => a.providerId);
|
||||
|
||||
const providerNameById = new Map(
|
||||
activeAttachments.map((a) => [a.providerId, a.name] as const)
|
||||
);
|
||||
|
||||
const models: EffectiveAllowModel[] = [];
|
||||
|
||||
if (inheritProviderIds.length > 0) {
|
||||
const rows = await db
|
||||
.select({
|
||||
modelId: aiModels.modelId,
|
||||
modelKey: aiModels.modelKey,
|
||||
name: aiModels.name,
|
||||
providerId: aiModels.providerId
|
||||
})
|
||||
.from(aiModels)
|
||||
.where(
|
||||
and(
|
||||
inArray(aiModels.providerId, inheritProviderIds),
|
||||
eq(aiModels.enabled, true),
|
||||
eq(aiModels.listType, "allow")
|
||||
)
|
||||
);
|
||||
for (const row of rows) {
|
||||
models.push({
|
||||
...row,
|
||||
providerName: providerNameById.get(row.providerId) ?? ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (selectProviderIds.length > 0) {
|
||||
if (options.resourceId !== undefined) {
|
||||
const rows = await db
|
||||
.select({
|
||||
modelId: aiModels.modelId,
|
||||
modelKey: aiModels.modelKey,
|
||||
name: aiModels.name,
|
||||
providerId: aiModels.providerId
|
||||
})
|
||||
.from(resourceAiModels)
|
||||
.innerJoin(
|
||||
aiModels,
|
||||
eq(resourceAiModels.modelId, aiModels.modelId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceAiModels.resourceId, options.resourceId),
|
||||
inArray(aiModels.providerId, selectProviderIds),
|
||||
eq(resourceAiModels.listType, "allow"),
|
||||
eq(aiModels.enabled, true)
|
||||
)
|
||||
);
|
||||
for (const row of rows) {
|
||||
models.push({
|
||||
...row,
|
||||
providerName: providerNameById.get(row.providerId) ?? ""
|
||||
});
|
||||
}
|
||||
} else if (options.siteResourceId !== undefined) {
|
||||
const rows = await db
|
||||
.select({
|
||||
modelId: aiModels.modelId,
|
||||
modelKey: aiModels.modelKey,
|
||||
name: aiModels.name,
|
||||
providerId: aiModels.providerId
|
||||
})
|
||||
.from(siteResourceAiModels)
|
||||
.innerJoin(
|
||||
aiModels,
|
||||
eq(siteResourceAiModels.modelId, aiModels.modelId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
siteResourceAiModels.siteResourceId,
|
||||
options.siteResourceId
|
||||
),
|
||||
inArray(aiModels.providerId, selectProviderIds),
|
||||
eq(siteResourceAiModels.listType, "allow"),
|
||||
eq(aiModels.enabled, true)
|
||||
)
|
||||
);
|
||||
for (const row of rows) {
|
||||
models.push({
|
||||
...row,
|
||||
providerName: providerNameById.get(row.providerId) ?? ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
models.sort((a, b) => {
|
||||
const byProvider = a.providerName.localeCompare(
|
||||
b.providerName,
|
||||
undefined,
|
||||
{
|
||||
sensitivity: "base"
|
||||
}
|
||||
);
|
||||
if (byProvider !== 0) {
|
||||
return byProvider;
|
||||
}
|
||||
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
||||
});
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model list APIs require an inference resource with at least one select-mode
|
||||
* attached provider.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user