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:
@@ -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,
|
||||
verifyOlmAccess,
|
||||
verifyLimits,
|
||||
verifyResourcePolicyAccess
|
||||
verifyResourcePolicyAccess,
|
||||
verifyAiProviderAccess,
|
||||
verifyAiModelAccess
|
||||
} from "@server/middlewares";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||
@@ -55,6 +57,7 @@ import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||
import { logActionAudit } from "#dynamic/middlewares";
|
||||
import { checkRoundTripMessage } from "./ws";
|
||||
import * as labels from "@server/routers/labels";
|
||||
import * as aiProvider from "@server/routers/aiProvider";
|
||||
|
||||
// Root routes
|
||||
export const unauthenticated = Router();
|
||||
@@ -1366,6 +1369,90 @@ authenticated.get(
|
||||
|
||||
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(
|
||||
"/org/:orgId/labels",
|
||||
verifyOrgAccess,
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as apiKeys from "./apiKeys";
|
||||
import * as idp from "./idp";
|
||||
import * as logs from "./auditLogs";
|
||||
import * as siteResource from "./siteResource";
|
||||
import * as aiProvider from "./aiProvider";
|
||||
import {
|
||||
verifyApiKey,
|
||||
verifyApiKeyOrgAccess,
|
||||
@@ -31,6 +32,8 @@ import {
|
||||
verifyLimits,
|
||||
verifyApiKeyDomainAccess,
|
||||
verifyApiKeyResourcePolicyAccess,
|
||||
verifyApiKeyAiProviderAccess,
|
||||
verifyApiKeyAiModelAccess,
|
||||
verifyUserHasAction
|
||||
} from "@server/middlewares";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -1366,3 +1369,87 @@ authenticated.get(
|
||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
||||
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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user