mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-01 10:11:27 +02:00
add crud for providers and models
This commit is contained in:
@@ -1424,6 +1424,16 @@
|
|||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
|
"actionCreateAiProvider": "Create AI Provider",
|
||||||
|
"actionDeleteAiProvider": "Delete AI Provider",
|
||||||
|
"actionGetAiProvider": "Get AI Provider",
|
||||||
|
"actionListAiProviders": "List AI Providers",
|
||||||
|
"actionUpdateAiProvider": "Update AI Provider",
|
||||||
|
"actionCreateAiModel": "Create AI Model",
|
||||||
|
"actionDeleteAiModel": "Delete AI Model",
|
||||||
|
"actionGetAiModel": "Get AI Model",
|
||||||
|
"actionListAiModels": "List AI Models",
|
||||||
|
"actionUpdateAiModel": "Update AI Model",
|
||||||
"actionApplyBlueprint": "Apply Blueprint",
|
"actionApplyBlueprint": "Apply Blueprint",
|
||||||
"actionListBlueprints": "List Blueprints",
|
"actionListBlueprints": "List Blueprints",
|
||||||
"actionGetBlueprint": "Get Blueprint",
|
"actionGetBlueprint": "Get Blueprint",
|
||||||
|
|||||||
+11
-1
@@ -182,7 +182,17 @@ export enum ActionsEnum {
|
|||||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||||
setResourcePolicyRules = "setResourcePolicyRules",
|
setResourcePolicyRules = "setResourcePolicyRules",
|
||||||
createOrgWideLauncherView = "createOrgWideLauncherView"
|
createOrgWideLauncherView = "createOrgWideLauncherView",
|
||||||
|
createAiProvider = "createAiProvider",
|
||||||
|
deleteAiProvider = "deleteAiProvider",
|
||||||
|
getAiProvider = "getAiProvider",
|
||||||
|
listAiProviders = "listAiProviders",
|
||||||
|
updateAiProvider = "updateAiProvider",
|
||||||
|
createAiModel = "createAiModel",
|
||||||
|
deleteAiModel = "deleteAiModel",
|
||||||
|
getAiModel = "getAiModel",
|
||||||
|
listAiModels = "listAiModels",
|
||||||
|
updateAiModel = "updateAiModel"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { createIntegrationApiServer } from "./integrationApiServer";
|
|||||||
import {
|
import {
|
||||||
ApiKey,
|
ApiKey,
|
||||||
ApiKeyOrg,
|
ApiKeyOrg,
|
||||||
|
AiModel,
|
||||||
|
AiProvider,
|
||||||
RemoteExitNode,
|
RemoteExitNode,
|
||||||
Session,
|
Session,
|
||||||
SiteResource,
|
SiteResource,
|
||||||
@@ -83,6 +85,8 @@ declare global {
|
|||||||
userOrgIds?: string[];
|
userOrgIds?: string[];
|
||||||
remoteExitNode?: RemoteExitNode;
|
remoteExitNode?: RemoteExitNode;
|
||||||
siteResource?: SiteResource;
|
siteResource?: SiteResource;
|
||||||
|
aiProvider?: AiProvider;
|
||||||
|
aiModel?: AiModel;
|
||||||
orgPolicyAllowed?: boolean;
|
orgPolicyAllowed?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
export type AiProviderType = "openai" | "anthropic" | "bedrock" | "custom";
|
||||||
|
export type AiProviderAuthType = "bearer";
|
||||||
|
export type AiBudgetUnit = "usd" | "tokens";
|
||||||
|
|
||||||
|
type AiProviderDefaults = {
|
||||||
|
upstreamUrl: string;
|
||||||
|
authType: AiProviderAuthType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AI_PROVIDER_DEFAULTS: Record<
|
||||||
|
Exclude<AiProviderType, "custom">,
|
||||||
|
AiProviderDefaults
|
||||||
|
> = {
|
||||||
|
openai: {
|
||||||
|
upstreamUrl: "https://api.openai.com/v1",
|
||||||
|
authType: "bearer"
|
||||||
|
},
|
||||||
|
anthropic: {
|
||||||
|
upstreamUrl: "https://api.anthropic.com",
|
||||||
|
authType: "bearer"
|
||||||
|
},
|
||||||
|
bedrock: {
|
||||||
|
upstreamUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||||
|
authType: "bearer"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveAiProviderConfig(input: {
|
||||||
|
type: AiProviderType;
|
||||||
|
upstreamUrl: string | null;
|
||||||
|
authType: AiProviderAuthType | null;
|
||||||
|
}): {
|
||||||
|
upstreamUrl: string | null;
|
||||||
|
authType: AiProviderAuthType | null;
|
||||||
|
} {
|
||||||
|
if (input.type === "custom") {
|
||||||
|
return {
|
||||||
|
upstreamUrl: input.upstreamUrl,
|
||||||
|
authType: input.authType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = AI_PROVIDER_DEFAULTS[input.type];
|
||||||
|
return {
|
||||||
|
upstreamUrl: input.upstreamUrl ?? defaults.upstreamUrl,
|
||||||
|
authType: input.authType ?? defaults.authType
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ export * from "./verifyUserHasAction";
|
|||||||
export * from "./verifyApiKeyAccess";
|
export * from "./verifyApiKeyAccess";
|
||||||
export * from "./verifySiteProvisioningKeyAccess";
|
export * from "./verifySiteProvisioningKeyAccess";
|
||||||
export * from "./verifyDomainAccess";
|
export * from "./verifyDomainAccess";
|
||||||
|
export * from "./verifyAiProviderAccess";
|
||||||
|
export * from "./verifyAiModelAccess";
|
||||||
export * from "./verifyUserIsOrgOwner";
|
export * from "./verifyUserIsOrgOwner";
|
||||||
export * from "./verifyUserFromResourceSession";
|
export * from "./verifyUserFromResourceSession";
|
||||||
export * from "./verifySiteResourceAccess";
|
export * from "./verifySiteResourceAccess";
|
||||||
|
|||||||
@@ -16,5 +16,7 @@ export * from "./verifyApiKeyClientAccess";
|
|||||||
export * from "./verifyApiKeySiteResourceAccess";
|
export * from "./verifyApiKeySiteResourceAccess";
|
||||||
export * from "./verifyApiKeyIdpAccess";
|
export * from "./verifyApiKeyIdpAccess";
|
||||||
export * from "./verifyApiKeyDomainAccess";
|
export * from "./verifyApiKeyDomainAccess";
|
||||||
|
export * from "./verifyApiKeyAiProviderAccess";
|
||||||
|
export * from "./verifyApiKeyAiModelAccess";
|
||||||
export * from "./verifyApiKeyResourcePolicyAccess";
|
export * from "./verifyApiKeyResourcePolicyAccess";
|
||||||
export * from "./verifyApiKeySiteProvisioningKeyAccess";
|
export * from "./verifyApiKeySiteProvisioningKeyAccess";
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiModels, aiProviders, apiKeyOrg, db } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyApiKeyAiModelAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const modelIdRaw = getFirstString(req.params.modelId);
|
||||||
|
const modelId = Number.parseInt(modelIdRaw ?? "", 10);
|
||||||
|
const providerIdRaw = getFirstString(req.params.providerId);
|
||||||
|
const providerId = Number.parseInt(providerIdRaw ?? "", 10);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(providerId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(modelId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid model ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({
|
||||||
|
model: aiModels,
|
||||||
|
provider: aiProviders
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(aiModels.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.modelId, modelId),
|
||||||
|
eq(aiModels.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI model with ID ${modelId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
req.aiProvider = row.provider;
|
||||||
|
req.aiModel = row.model;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.aiProvider = row.provider;
|
||||||
|
req.aiModel = row.model;
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI model access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiProviders, apiKeyOrg, db } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyApiKeyAiProviderAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const providerIdRaw = getFirstString(req.params.providerId);
|
||||||
|
const providerId = Number.parseInt(providerIdRaw ?? "", 10);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(providerId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
const [provider] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.aiProvider = provider;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [provider] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.aiProvider = provider;
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI provider access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiModels, aiProviders, db, userOrgs } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyAiModelAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const modelIdRaw = getFirstString(req.params.modelId);
|
||||||
|
const modelId = Number.parseInt(modelIdRaw ?? "", 10);
|
||||||
|
const providerIdRaw = getFirstString(req.params.providerId);
|
||||||
|
const providerId = Number.parseInt(providerIdRaw ?? "", 10);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(providerId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(modelId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid model ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({
|
||||||
|
model: aiModels,
|
||||||
|
provider: aiProviders
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(aiModels.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.modelId, modelId),
|
||||||
|
eq(aiModels.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI model with ID ${modelId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
const userOrgRole = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.userOrg = userOrgRole[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"" + (policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
|
||||||
|
req.aiProvider = row.provider;
|
||||||
|
req.aiModel = row.model;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI model access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiProviders, db, userOrgs } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyAiProviderAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const providerIdRaw = getFirstString(req.params.providerId);
|
||||||
|
const providerId = Number.parseInt(providerIdRaw ?? "", 10);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(providerId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [provider] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
const userOrgRole = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.userOrg = userOrgRole[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"" + (policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
|
||||||
|
req.aiProvider = provider;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI provider access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -28,7 +28,9 @@ export enum OpenAPITags {
|
|||||||
HealthCheck = "Health Check",
|
HealthCheck = "Health Check",
|
||||||
PublicResourcePolicyLegacy = "Public Resource Policy (Legacy)",
|
PublicResourcePolicyLegacy = "Public Resource Policy (Legacy)",
|
||||||
PublicResourceLegacy = "Public Resource (Legacy)",
|
PublicResourceLegacy = "Public Resource (Legacy)",
|
||||||
PrivateResourceLegacy = "Private Resource (Legacy)"
|
PrivateResourceLegacy = "Private Resource (Legacy)",
|
||||||
|
AiProvider = "AI Provider",
|
||||||
|
AiModel = "AI Model"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order here controls the order tags are displayed in Swagger UI
|
// Order here controls the order tags are displayed in Swagger UI
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiModels, aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import type { CreateOrEditAiModelResponse } from "@server/routers/aiProvider/types";
|
||||||
|
import {
|
||||||
|
aiBudgetUnitSchema,
|
||||||
|
refineBudgetFields
|
||||||
|
} from "@server/routers/aiProvider/validation";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z
|
||||||
|
.strictObject({
|
||||||
|
modelKey: z.string().nonempty(),
|
||||||
|
name: z.string().nonempty(),
|
||||||
|
budgetAmount: z.number().positive().optional().nullable(),
|
||||||
|
budgetUnit: aiBudgetUnitSchema.optional().nullable(),
|
||||||
|
enabled: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
refineBudgetFields(data, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}/model",
|
||||||
|
description: "Create an AI model under a provider.",
|
||||||
|
tags: [OpenAPITags.AiModel],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
201: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createAiModel(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId } = parsedParams.data;
|
||||||
|
const { modelKey, name, budgetAmount, budgetUnit, enabled } =
|
||||||
|
parsedBody.data;
|
||||||
|
|
||||||
|
const [provider] =
|
||||||
|
req.aiProvider && req.aiProvider.providerId === providerId
|
||||||
|
? [req.aiProvider]
|
||||||
|
: await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ modelId: aiModels.modelId })
|
||||||
|
.from(aiModels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.providerId, providerId),
|
||||||
|
eq(aiModels.modelKey, modelKey)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
`Model with key ${modelKey} already exists for this provider`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const [model] = await db
|
||||||
|
.insert(aiModels)
|
||||||
|
.values({
|
||||||
|
providerId,
|
||||||
|
modelKey,
|
||||||
|
name,
|
||||||
|
budgetAmount: budgetAmount ?? null,
|
||||||
|
budgetUnit: budgetUnit ?? null,
|
||||||
|
enabled: enabled ?? true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response<CreateOrEditAiModelResponse>(res, {
|
||||||
|
data: { model },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI model created successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { encrypt } from "@server/lib/crypto";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
||||||
|
import { toPublicAiProvider } from "@server/routers/aiProvider/types";
|
||||||
|
import {
|
||||||
|
aiAuthTypeSchema,
|
||||||
|
aiBudgetUnitSchema,
|
||||||
|
aiProviderTypeSchema,
|
||||||
|
refineBudgetFields,
|
||||||
|
refineCustomProviderFields
|
||||||
|
} from "@server/routers/aiProvider/validation";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z
|
||||||
|
.strictObject({
|
||||||
|
name: z.string().nonempty(),
|
||||||
|
type: aiProviderTypeSchema,
|
||||||
|
upstreamUrl: z.url().optional().nullable(),
|
||||||
|
apiKey: z.string().optional(),
|
||||||
|
authType: aiAuthTypeSchema.optional().nullable(),
|
||||||
|
skipTlsVerification: z.boolean().optional(),
|
||||||
|
budgetAmount: z.number().positive().optional().nullable(),
|
||||||
|
budgetUnit: aiBudgetUnitSchema.optional().nullable(),
|
||||||
|
enabled: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
refineCustomProviderFields(data, ctx);
|
||||||
|
refineBudgetFields(data, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/org/{orgId}/ai-provider",
|
||||||
|
description: "Create an AI provider for an organization.",
|
||||||
|
tags: [OpenAPITags.AiProvider],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
201: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createAiProvider(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId } = parsedParams.data;
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
upstreamUrl,
|
||||||
|
apiKey,
|
||||||
|
authType,
|
||||||
|
skipTlsVerification,
|
||||||
|
budgetAmount,
|
||||||
|
budgetUnit,
|
||||||
|
enabled
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
const encryptedApiKey = apiKey ? encrypt(apiKey, key) : null;
|
||||||
|
const apiKeyLastChars = apiKey ? apiKey.slice(-4) : null;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const [provider] = await db
|
||||||
|
.insert(aiProviders)
|
||||||
|
.values({
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
upstreamUrl: upstreamUrl ?? null,
|
||||||
|
apiKey: encryptedApiKey,
|
||||||
|
apiKeyLastChars,
|
||||||
|
authType: authType ?? null,
|
||||||
|
skipTlsVerification: skipTlsVerification ?? false,
|
||||||
|
budgetAmount: budgetAmount ?? null,
|
||||||
|
budgetUnit: budgetUnit ?? null,
|
||||||
|
enabled: enabled ?? true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response<CreateOrEditAiProviderResponse>(res, {
|
||||||
|
data: { provider: toPublicAiProvider(provider) },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI provider created successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiModels, aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive(),
|
||||||
|
modelId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "delete",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}/model/{modelId}",
|
||||||
|
description: "Delete an AI model.",
|
||||||
|
tags: [OpenAPITags.AiModel],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function deleteAiModel(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId, modelId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ modelId: aiModels.modelId })
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(aiModels.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.modelId, modelId),
|
||||||
|
eq(aiModels.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI model with ID ${modelId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(aiModels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.modelId, modelId),
|
||||||
|
eq(aiModels.providerId, providerId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI model deleted successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "delete",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}",
|
||||||
|
description: "Delete an AI provider.",
|
||||||
|
tags: [OpenAPITags.AiProvider],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function deleteAiProvider(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ providerId: aiProviders.providerId })
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI provider deleted successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiModels, aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import type { GetAiModelResponse } from "@server/routers/aiProvider/types";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive(),
|
||||||
|
modelId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}/model/{modelId}",
|
||||||
|
description: "Get an AI model by ID.",
|
||||||
|
tags: [OpenAPITags.AiModel],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getAiModel(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId, modelId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [model] =
|
||||||
|
req.aiModel && req.aiModel.modelId === modelId
|
||||||
|
? [req.aiModel]
|
||||||
|
: await db
|
||||||
|
.select({ model: aiModels })
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(aiModels.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.modelId, modelId),
|
||||||
|
eq(aiModels.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.then((rows) => rows.map((r) => r.model));
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI model with ID ${modelId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<GetAiModelResponse>(res, {
|
||||||
|
data: { model },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI model retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import type { GetAiProviderResponse } from "@server/routers/aiProvider/types";
|
||||||
|
import { toPublicAiProvider } from "@server/routers/aiProvider/types";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}",
|
||||||
|
description: "Get an AI provider by ID.",
|
||||||
|
tags: [OpenAPITags.AiProvider],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getAiProvider(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [provider] =
|
||||||
|
req.aiProvider && req.aiProvider.providerId === providerId
|
||||||
|
? [req.aiProvider]
|
||||||
|
: await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<GetAiProviderResponse>(res, {
|
||||||
|
data: { provider: toPublicAiProvider(provider) },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI provider retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export * from "./createAiProvider";
|
||||||
|
export * from "./listAiProviders";
|
||||||
|
export * from "./getAiProvider";
|
||||||
|
export * from "./updateAiProvider";
|
||||||
|
export * from "./deleteAiProvider";
|
||||||
|
export * from "./createAiModel";
|
||||||
|
export * from "./listAiModels";
|
||||||
|
export * from "./getAiModel";
|
||||||
|
export * from "./updateAiModel";
|
||||||
|
export * from "./deleteAiModel";
|
||||||
|
export * from "./types";
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiModels, aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, asc, eq, like, sql } from "drizzle-orm";
|
||||||
|
import type { ListAiModelsResponse } from "@server/routers/aiProvider/types";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
const listSchema = z.object({
|
||||||
|
pageSize: z.coerce
|
||||||
|
.number<string>()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.optional()
|
||||||
|
.catch(20)
|
||||||
|
.default(20)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 20,
|
||||||
|
description: "Number of items per page"
|
||||||
|
}),
|
||||||
|
page: z.coerce
|
||||||
|
.number<string>()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.optional()
|
||||||
|
.catch(1)
|
||||||
|
.default(1)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 1,
|
||||||
|
description: "Page number to retrieve"
|
||||||
|
}),
|
||||||
|
query: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}/models",
|
||||||
|
description: "List AI models for a provider.",
|
||||||
|
tags: [OpenAPITags.AiModel],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
query: listSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function listAiModels(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedQuery = listSchema.safeParse(req.query);
|
||||||
|
if (!parsedQuery.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedQuery.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [provider] =
|
||||||
|
req.aiProvider && req.aiProvider.providerId === providerId
|
||||||
|
? [req.aiProvider]
|
||||||
|
: await db
|
||||||
|
.select({ providerId: aiProviders.providerId })
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pageSize, page, query } = parsedQuery.data;
|
||||||
|
const conditions = [eq(aiModels.providerId, providerId)];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
conditions.push(
|
||||||
|
like(
|
||||||
|
sql`LOWER(${aiModels.name})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseQuery = db
|
||||||
|
.select()
|
||||||
|
.from(aiModels)
|
||||||
|
.where(and(...conditions));
|
||||||
|
|
||||||
|
const countQuery = db.$count(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(aiModels)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.as("filtered_ai_models")
|
||||||
|
);
|
||||||
|
|
||||||
|
const [totalCount, rows] = await Promise.all([
|
||||||
|
countQuery,
|
||||||
|
baseQuery
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(pageSize * (page - 1))
|
||||||
|
.orderBy(asc(aiModels.name))
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response<ListAiModelsResponse>(res, {
|
||||||
|
data: {
|
||||||
|
models: rows,
|
||||||
|
pagination: {
|
||||||
|
total: totalCount,
|
||||||
|
pageSize,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI models retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, asc, eq, like, sql } from "drizzle-orm";
|
||||||
|
import type { ListAiProvidersResponse } from "@server/routers/aiProvider/types";
|
||||||
|
import { toPublicAiProvider } from "@server/routers/aiProvider/types";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
const listSchema = z.object({
|
||||||
|
pageSize: z.coerce
|
||||||
|
.number<string>()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.optional()
|
||||||
|
.catch(20)
|
||||||
|
.default(20)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 20,
|
||||||
|
description: "Number of items per page"
|
||||||
|
}),
|
||||||
|
page: z.coerce
|
||||||
|
.number<string>()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.optional()
|
||||||
|
.catch(1)
|
||||||
|
.default(1)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 1,
|
||||||
|
description: "Page number to retrieve"
|
||||||
|
}),
|
||||||
|
query: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/ai-providers",
|
||||||
|
description: "List AI providers for an organization.",
|
||||||
|
tags: [OpenAPITags.AiProvider],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
query: listSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function listAiProviders(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedQuery = listSchema.safeParse(req.query);
|
||||||
|
if (!parsedQuery.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedQuery.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pageSize, page, query } = parsedQuery.data;
|
||||||
|
const conditions = [eq(aiProviders.orgId, orgId)];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
conditions.push(
|
||||||
|
like(
|
||||||
|
sql`LOWER(${aiProviders.name})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseQuery = db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(and(...conditions));
|
||||||
|
|
||||||
|
const countQuery = db.$count(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.as("filtered_ai_providers")
|
||||||
|
);
|
||||||
|
|
||||||
|
const [totalCount, rows] = await Promise.all([
|
||||||
|
countQuery,
|
||||||
|
baseQuery
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(pageSize * (page - 1))
|
||||||
|
.orderBy(asc(aiProviders.name))
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response<ListAiProvidersResponse>(res, {
|
||||||
|
data: {
|
||||||
|
providers: rows.map(toPublicAiProvider),
|
||||||
|
pagination: {
|
||||||
|
total: totalCount,
|
||||||
|
pageSize,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI providers retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { AiModel, AiProvider } from "@server/db";
|
||||||
|
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||||
|
import {
|
||||||
|
resolveAiProviderConfig,
|
||||||
|
type AiProviderAuthType,
|
||||||
|
type AiProviderType
|
||||||
|
} from "@server/lib/aiProviderDefaults";
|
||||||
|
|
||||||
|
export type AiProviderPublic = Omit<AiProvider, "apiKey"> & {
|
||||||
|
effectiveUpstreamUrl: string | null;
|
||||||
|
effectiveAuthType: AiProviderAuthType | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListAiProvidersResponse = PaginatedResponse<{
|
||||||
|
providers: AiProviderPublic[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetAiProviderResponse = {
|
||||||
|
provider: AiProviderPublic;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateOrEditAiProviderResponse = {
|
||||||
|
provider: AiProviderPublic;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListAiModelsResponse = PaginatedResponse<{
|
||||||
|
models: AiModel[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetAiModelResponse = {
|
||||||
|
model: AiModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateOrEditAiModelResponse = {
|
||||||
|
model: AiModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toPublicAiProvider(provider: AiProvider): AiProviderPublic {
|
||||||
|
const { apiKey: _apiKey, ...rest } = provider;
|
||||||
|
const resolved = resolveAiProviderConfig({
|
||||||
|
type: provider.type as AiProviderType,
|
||||||
|
upstreamUrl: provider.upstreamUrl,
|
||||||
|
authType: provider.authType as AiProviderAuthType | null
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
effectiveUpstreamUrl: resolved.upstreamUrl,
|
||||||
|
effectiveAuthType: resolved.authType
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiModels, aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, eq, ne } from "drizzle-orm";
|
||||||
|
import type { CreateOrEditAiModelResponse } from "@server/routers/aiProvider/types";
|
||||||
|
import {
|
||||||
|
aiBudgetUnitSchema,
|
||||||
|
refineBudgetFields
|
||||||
|
} from "@server/routers/aiProvider/validation";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive(),
|
||||||
|
modelId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z
|
||||||
|
.strictObject({
|
||||||
|
modelKey: z.string().nonempty().optional(),
|
||||||
|
name: z.string().nonempty().optional(),
|
||||||
|
budgetAmount: z.number().positive().optional().nullable(),
|
||||||
|
budgetUnit: aiBudgetUnitSchema.optional().nullable(),
|
||||||
|
enabled: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
refineBudgetFields(data, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}/model/{modelId}",
|
||||||
|
description: "Update an AI model.",
|
||||||
|
tags: [OpenAPITags.AiModel],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function updateAiModel(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId, modelId } = parsedParams.data;
|
||||||
|
const body = parsedBody.data;
|
||||||
|
|
||||||
|
const [existing] =
|
||||||
|
req.aiModel && req.aiModel.modelId === modelId
|
||||||
|
? [req.aiModel]
|
||||||
|
: await db
|
||||||
|
.select({ model: aiModels })
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(aiModels.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.modelId, modelId),
|
||||||
|
eq(aiModels.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.then((rows) => rows.map((r) => r.model));
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI model with ID ${modelId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
body.modelKey !== undefined &&
|
||||||
|
body.modelKey !== existing.modelKey
|
||||||
|
) {
|
||||||
|
const [conflict] = await db
|
||||||
|
.select({ modelId: aiModels.modelId })
|
||||||
|
.from(aiModels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.providerId, providerId),
|
||||||
|
eq(aiModels.modelKey, body.modelKey),
|
||||||
|
ne(aiModels.modelId, modelId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
`Model with key ${body.modelKey} already exists for this provider`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: Partial<typeof aiModels.$inferInsert> = {
|
||||||
|
updatedAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.modelKey !== undefined) {
|
||||||
|
updateData.modelKey = body.modelKey;
|
||||||
|
}
|
||||||
|
if (body.name !== undefined) {
|
||||||
|
updateData.name = body.name;
|
||||||
|
}
|
||||||
|
if (body.budgetAmount !== undefined) {
|
||||||
|
updateData.budgetAmount = body.budgetAmount;
|
||||||
|
}
|
||||||
|
if (body.budgetUnit !== undefined) {
|
||||||
|
updateData.budgetUnit = body.budgetUnit;
|
||||||
|
}
|
||||||
|
if (body.enabled !== undefined) {
|
||||||
|
updateData.enabled = body.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [model] = await db
|
||||||
|
.update(aiModels)
|
||||||
|
.set(updateData)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.modelId, modelId),
|
||||||
|
eq(aiModels.providerId, providerId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response<CreateOrEditAiModelResponse>(res, {
|
||||||
|
data: { model },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI model updated successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { aiProviders, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { encrypt } from "@server/lib/crypto";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
||||||
|
import { toPublicAiProvider } from "@server/routers/aiProvider/types";
|
||||||
|
import type { AiProviderType } from "@server/lib/aiProviderDefaults";
|
||||||
|
import {
|
||||||
|
aiAuthTypeSchema,
|
||||||
|
aiBudgetUnitSchema,
|
||||||
|
refineBudgetFields,
|
||||||
|
refineCustomProviderFields
|
||||||
|
} from "@server/routers/aiProvider/validation";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
providerId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z
|
||||||
|
.strictObject({
|
||||||
|
name: z.string().nonempty().optional(),
|
||||||
|
upstreamUrl: z.url().optional().nullable(),
|
||||||
|
apiKey: z.string().optional(),
|
||||||
|
authType: aiAuthTypeSchema.optional().nullable(),
|
||||||
|
skipTlsVerification: z.boolean().optional(),
|
||||||
|
budgetAmount: z.number().positive().optional().nullable(),
|
||||||
|
budgetUnit: aiBudgetUnitSchema.optional().nullable(),
|
||||||
|
enabled: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
refineBudgetFields(data, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/org/{orgId}/ai-provider/{providerId}",
|
||||||
|
description: "Update an AI provider.",
|
||||||
|
tags: [OpenAPITags.AiProvider],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function updateAiProvider(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, providerId } = parsedParams.data;
|
||||||
|
const body = parsedBody.data;
|
||||||
|
|
||||||
|
const [existing] =
|
||||||
|
req.aiProvider && req.aiProvider.providerId === providerId
|
||||||
|
? [req.aiProvider]
|
||||||
|
: await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerType = existing.type as AiProviderType;
|
||||||
|
|
||||||
|
if (providerType === "custom") {
|
||||||
|
const nextUpstreamUrl =
|
||||||
|
body.upstreamUrl !== undefined
|
||||||
|
? body.upstreamUrl
|
||||||
|
: existing.upstreamUrl;
|
||||||
|
const nextAuthType =
|
||||||
|
body.authType !== undefined ? body.authType : existing.authType;
|
||||||
|
|
||||||
|
const validation = z
|
||||||
|
.object({
|
||||||
|
type: z.literal("custom"),
|
||||||
|
upstreamUrl: z.string().nullable().optional(),
|
||||||
|
authType: aiAuthTypeSchema.nullable().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) =>
|
||||||
|
refineCustomProviderFields(data, ctx)
|
||||||
|
)
|
||||||
|
.safeParse({
|
||||||
|
type: "custom",
|
||||||
|
upstreamUrl: nextUpstreamUrl,
|
||||||
|
authType: nextAuthType
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(validation.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: Partial<typeof aiProviders.$inferInsert> = {
|
||||||
|
updatedAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.name !== undefined) {
|
||||||
|
updateData.name = body.name;
|
||||||
|
}
|
||||||
|
if (body.skipTlsVerification !== undefined) {
|
||||||
|
updateData.skipTlsVerification = body.skipTlsVerification;
|
||||||
|
}
|
||||||
|
if (body.enabled !== undefined) {
|
||||||
|
updateData.enabled = body.enabled;
|
||||||
|
}
|
||||||
|
if (body.budgetAmount !== undefined) {
|
||||||
|
updateData.budgetAmount = body.budgetAmount;
|
||||||
|
}
|
||||||
|
if (body.budgetUnit !== undefined) {
|
||||||
|
updateData.budgetUnit = body.budgetUnit;
|
||||||
|
}
|
||||||
|
if (body.upstreamUrl !== undefined) {
|
||||||
|
updateData.upstreamUrl = body.upstreamUrl;
|
||||||
|
}
|
||||||
|
if (body.authType !== undefined) {
|
||||||
|
updateData.authType = body.authType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.apiKey !== undefined) {
|
||||||
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
updateData.apiKey = encrypt(body.apiKey, key);
|
||||||
|
updateData.apiKeyLastChars = body.apiKey.slice(-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [provider] = await db
|
||||||
|
.update(aiProviders)
|
||||||
|
.set(updateData)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.providerId, providerId),
|
||||||
|
eq(aiProviders.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response<CreateOrEditAiProviderResponse>(res, {
|
||||||
|
data: { provider: toPublicAiProvider(provider) },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "AI provider updated successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import type {
|
||||||
|
AiBudgetUnit,
|
||||||
|
AiProviderType
|
||||||
|
} from "@server/lib/aiProviderDefaults";
|
||||||
|
|
||||||
|
export const aiProviderTypeSchema = z.enum([
|
||||||
|
"openai",
|
||||||
|
"anthropic",
|
||||||
|
"bedrock",
|
||||||
|
"custom"
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const aiBudgetUnitSchema = z.enum(["usd", "tokens"]);
|
||||||
|
|
||||||
|
export const aiAuthTypeSchema = z.enum(["bearer"]);
|
||||||
|
|
||||||
|
export function refineBudgetFields(
|
||||||
|
data: {
|
||||||
|
budgetAmount?: number | null;
|
||||||
|
budgetUnit?: AiBudgetUnit | null;
|
||||||
|
},
|
||||||
|
ctx: z.RefinementCtx
|
||||||
|
) {
|
||||||
|
const hasAmount =
|
||||||
|
data.budgetAmount !== undefined && data.budgetAmount !== null;
|
||||||
|
const hasUnit = data.budgetUnit !== undefined && data.budgetUnit !== null;
|
||||||
|
|
||||||
|
if (hasAmount !== hasUnit) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message:
|
||||||
|
"budgetAmount and budgetUnit must both be set or both omitted",
|
||||||
|
path: hasAmount ? ["budgetUnit"] : ["budgetAmount"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refineCustomProviderFields(
|
||||||
|
data: {
|
||||||
|
type: AiProviderType;
|
||||||
|
upstreamUrl?: string | null;
|
||||||
|
authType?: "bearer" | null;
|
||||||
|
},
|
||||||
|
ctx: z.RefinementCtx
|
||||||
|
) {
|
||||||
|
if (data.type !== "custom") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.upstreamUrl) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "upstreamUrl is required for custom providers",
|
||||||
|
path: ["upstreamUrl"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.authType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "authType is required for custom providers",
|
||||||
|
path: ["authType"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,9 @@ import {
|
|||||||
verifySiteResourceAccess,
|
verifySiteResourceAccess,
|
||||||
verifyOlmAccess,
|
verifyOlmAccess,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyResourcePolicyAccess
|
verifyResourcePolicyAccess,
|
||||||
|
verifyAiProviderAccess,
|
||||||
|
verifyAiModelAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
@@ -55,6 +57,7 @@ import { createStore } from "#dynamic/lib/rateLimitStore";
|
|||||||
import { logActionAudit } from "#dynamic/middlewares";
|
import { logActionAudit } from "#dynamic/middlewares";
|
||||||
import { checkRoundTripMessage } from "./ws";
|
import { checkRoundTripMessage } from "./ws";
|
||||||
import * as labels from "@server/routers/labels";
|
import * as labels from "@server/routers/labels";
|
||||||
|
import * as aiProvider from "@server/routers/aiProvider";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
export const unauthenticated = Router();
|
export const unauthenticated = Router();
|
||||||
@@ -1366,6 +1369,90 @@ authenticated.get(
|
|||||||
|
|
||||||
authenticated.get("/ws/round-trip-message/:messageId", checkRoundTripMessage);
|
authenticated.get("/ws/round-trip-message/:messageId", checkRoundTripMessage);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/ai-provider",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.createAiProvider),
|
||||||
|
logActionAudit(ActionsEnum.createAiProvider),
|
||||||
|
aiProvider.createAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-providers",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listAiProviders),
|
||||||
|
aiProvider.listAiProviders
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-provider/:providerId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiProviderAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getAiProvider),
|
||||||
|
aiProvider.getAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/ai-provider/:providerId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiProviderAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateAiProvider),
|
||||||
|
logActionAudit(ActionsEnum.updateAiProvider),
|
||||||
|
aiProvider.updateAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/ai-provider/:providerId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiProviderAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.deleteAiProvider),
|
||||||
|
logActionAudit(ActionsEnum.deleteAiProvider),
|
||||||
|
aiProvider.deleteAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiProviderAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.createAiModel),
|
||||||
|
logActionAudit(ActionsEnum.createAiModel),
|
||||||
|
aiProvider.createAiModel
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/models",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiProviderAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listAiModels),
|
||||||
|
aiProvider.listAiModels
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model/:modelId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiModelAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getAiModel),
|
||||||
|
aiProvider.getAiModel
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model/:modelId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiModelAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateAiModel),
|
||||||
|
logActionAudit(ActionsEnum.updateAiModel),
|
||||||
|
aiProvider.updateAiModel
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model/:modelId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyAiModelAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.deleteAiModel),
|
||||||
|
logActionAudit(ActionsEnum.deleteAiModel),
|
||||||
|
aiProvider.deleteAiModel
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/labels",
|
"/org/:orgId/labels",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import * as apiKeys from "./apiKeys";
|
|||||||
import * as idp from "./idp";
|
import * as idp from "./idp";
|
||||||
import * as logs from "./auditLogs";
|
import * as logs from "./auditLogs";
|
||||||
import * as siteResource from "./siteResource";
|
import * as siteResource from "./siteResource";
|
||||||
|
import * as aiProvider from "./aiProvider";
|
||||||
import {
|
import {
|
||||||
verifyApiKey,
|
verifyApiKey,
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
@@ -31,6 +32,8 @@ import {
|
|||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyApiKeyDomainAccess,
|
verifyApiKeyDomainAccess,
|
||||||
verifyApiKeyResourcePolicyAccess,
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyApiKeyAiProviderAccess,
|
||||||
|
verifyApiKeyAiModelAccess,
|
||||||
verifyUserHasAction
|
verifyUserHasAction
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -1366,3 +1369,87 @@ authenticated.get(
|
|||||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
verifyApiKeyHasAction(ActionsEnum.listResources),
|
||||||
resource.listAllResourceNames
|
resource.listAllResourceNames
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/ai-provider",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createAiProvider),
|
||||||
|
logActionAudit(ActionsEnum.createAiProvider),
|
||||||
|
aiProvider.createAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-providers",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listAiProviders),
|
||||||
|
aiProvider.listAiProviders
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-provider/:providerId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiProviderAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getAiProvider),
|
||||||
|
aiProvider.getAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/ai-provider/:providerId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiProviderAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateAiProvider),
|
||||||
|
logActionAudit(ActionsEnum.updateAiProvider),
|
||||||
|
aiProvider.updateAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/ai-provider/:providerId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiProviderAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteAiProvider),
|
||||||
|
logActionAudit(ActionsEnum.deleteAiProvider),
|
||||||
|
aiProvider.deleteAiProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiProviderAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createAiModel),
|
||||||
|
logActionAudit(ActionsEnum.createAiModel),
|
||||||
|
aiProvider.createAiModel
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/models",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiProviderAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listAiModels),
|
||||||
|
aiProvider.listAiModels
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model/:modelId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiModelAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getAiModel),
|
||||||
|
aiProvider.getAiModel
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model/:modelId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiModelAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateAiModel),
|
||||||
|
logActionAudit(ActionsEnum.updateAiModel),
|
||||||
|
aiProvider.updateAiModel
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/ai-provider/:providerId/model/:modelId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyAiModelAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteAiModel),
|
||||||
|
logActionAudit(ActionsEnum.deleteAiModel),
|
||||||
|
aiProvider.deleteAiModel
|
||||||
|
);
|
||||||
|
|||||||
@@ -150,6 +150,22 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionListSiteProvisioningKeys")]: "listSiteProvisioningKeys",
|
[t("actionListSiteProvisioningKeys")]: "listSiteProvisioningKeys",
|
||||||
[t("actionUpdateSiteProvisioningKey")]: "updateSiteProvisioningKey",
|
[t("actionUpdateSiteProvisioningKey")]: "updateSiteProvisioningKey",
|
||||||
[t("actionDeleteSiteProvisioningKey")]: "deleteSiteProvisioningKey"
|
[t("actionDeleteSiteProvisioningKey")]: "deleteSiteProvisioningKey"
|
||||||
|
},
|
||||||
|
|
||||||
|
"AI Provider": {
|
||||||
|
[t("actionCreateAiProvider")]: "createAiProvider",
|
||||||
|
[t("actionDeleteAiProvider")]: "deleteAiProvider",
|
||||||
|
[t("actionGetAiProvider")]: "getAiProvider",
|
||||||
|
[t("actionListAiProviders")]: "listAiProviders",
|
||||||
|
[t("actionUpdateAiProvider")]: "updateAiProvider"
|
||||||
|
},
|
||||||
|
|
||||||
|
"AI Model": {
|
||||||
|
[t("actionCreateAiModel")]: "createAiModel",
|
||||||
|
[t("actionDeleteAiModel")]: "deleteAiModel",
|
||||||
|
[t("actionGetAiModel")]: "getAiModel",
|
||||||
|
[t("actionListAiModels")]: "listAiModels",
|
||||||
|
[t("actionUpdateAiModel")]: "updateAiModel"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user