diff --git a/server/middlewares/integration/verifyApiKeyAiProviderAccess.ts b/server/middlewares/integration/verifyApiKeyAiProviderAccess.ts
index 1668b5727..91cfa47e4 100644
--- a/server/middlewares/integration/verifyApiKeyAiProviderAccess.ts
+++ b/server/middlewares/integration/verifyApiKeyAiProviderAccess.ts
@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express";
-import { aiProviders, apiKeyOrg, db } from "@server/db";
+import { AiProvider, aiProviders, apiKeyOrg, db } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
@@ -13,7 +13,8 @@ export async function verifyApiKeyAiProviderAccess(
try {
const apiKey = req.apiKey;
const providerIdRaw = getFirstString(req.params.providerId);
- const providerId = Number.parseInt(providerIdRaw ?? "", 10);
+ const niceId = getFirstString(req.params.niceId);
+ const orgIdParam = getFirstString(req.params.orgId);
if (!apiKey) {
return next(
@@ -21,23 +22,42 @@ export async function verifyApiKeyAiProviderAccess(
);
}
- if (Number.isNaN(providerId)) {
- return next(
- createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
- );
- }
+ let provider: AiProvider | undefined;
- const [provider] = await db
- .select()
- .from(aiProviders)
- .where(eq(aiProviders.providerId, providerId))
- .limit(1);
+ if (niceId && orgIdParam) {
+ const [providerRes] = await db
+ .select()
+ .from(aiProviders)
+ .where(
+ and(
+ eq(aiProviders.niceId, niceId),
+ eq(aiProviders.orgId, orgIdParam)
+ )
+ )
+ .limit(1);
+ provider = providerRes;
+ } else {
+ const providerId = Number.parseInt(providerIdRaw ?? "", 10);
+
+ if (Number.isNaN(providerId)) {
+ return next(
+ createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
+ );
+ }
+
+ const [providerRes] = await db
+ .select()
+ .from(aiProviders)
+ .where(eq(aiProviders.providerId, providerId))
+ .limit(1);
+ provider = providerRes;
+ }
if (!provider) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
- `AI provider with ID ${providerId} not found`
+ `AI provider with ID ${providerIdRaw || niceId} not found`
)
);
}
diff --git a/server/middlewares/verifyAiProviderAccess.ts b/server/middlewares/verifyAiProviderAccess.ts
index 22edd6f86..057497cc3 100644
--- a/server/middlewares/verifyAiProviderAccess.ts
+++ b/server/middlewares/verifyAiProviderAccess.ts
@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express";
-import { aiProviders, db, userOrgs } from "@server/db";
+import { AiProvider, aiProviders, db, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
@@ -15,7 +15,8 @@ export async function verifyAiProviderAccess(
try {
const userId = req.user!.userId;
const providerIdRaw = getFirstString(req.params.providerId);
- const providerId = Number.parseInt(providerIdRaw ?? "", 10);
+ const niceId = getFirstString(req.params.niceId);
+ const orgIdParam = getFirstString(req.params.orgId);
if (!userId) {
return next(
@@ -23,23 +24,42 @@ export async function verifyAiProviderAccess(
);
}
- if (Number.isNaN(providerId)) {
- return next(
- createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
- );
- }
+ let provider: AiProvider | undefined;
- const [provider] = await db
- .select()
- .from(aiProviders)
- .where(eq(aiProviders.providerId, providerId))
- .limit(1);
+ if (niceId && orgIdParam) {
+ const [providerRes] = await db
+ .select()
+ .from(aiProviders)
+ .where(
+ and(
+ eq(aiProviders.niceId, niceId),
+ eq(aiProviders.orgId, orgIdParam)
+ )
+ )
+ .limit(1);
+ provider = providerRes;
+ } else {
+ const providerId = Number.parseInt(providerIdRaw ?? "", 10);
+
+ if (Number.isNaN(providerId)) {
+ return next(
+ createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
+ );
+ }
+
+ const [providerRes] = await db
+ .select()
+ .from(aiProviders)
+ .where(eq(aiProviders.providerId, providerId))
+ .limit(1);
+ provider = providerRes;
+ }
if (!provider) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
- `AI provider with ID ${providerId} not found`
+ `AI provider with ID ${providerIdRaw || niceId} not found`
)
);
}
diff --git a/server/routers/aiProvider/getAiProvider.ts b/server/routers/aiProvider/getAiProvider.ts
index 28a0152e1..53a3e099a 100644
--- a/server/routers/aiProvider/getAiProvider.ts
+++ b/server/routers/aiProvider/getAiProvider.ts
@@ -2,26 +2,78 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { aiProviders, db } from "@server/db";
import response from "@server/lib/response";
+import stoi from "@server/lib/stoi";
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 { eq } from "drizzle-orm";
+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({
- providerId: z.coerce.number().int().positive()
+ providerId: z
+ .string()
+ .optional()
+ .transform(stoi)
+ .pipe(z.int().positive().optional())
+ .optional(),
+ niceId: z.string().optional(),
+ orgId: z.string().optional()
});
+async function query(providerId?: number, niceId?: string, orgId?: string) {
+ if (providerId) {
+ const [res] = await db
+ .select()
+ .from(aiProviders)
+ .where(eq(aiProviders.providerId, providerId))
+ .limit(1);
+ return res;
+ } else if (niceId && orgId) {
+ const [res] = await db
+ .select()
+ .from(aiProviders)
+ .where(
+ and(
+ eq(aiProviders.niceId, niceId),
+ eq(aiProviders.orgId, orgId)
+ )
+ )
+ .limit(1);
+ return res;
+ }
+}
+
registry.registerPath({
method: "get",
path: "/ai-provider/{providerId}",
description: "Get an AI provider by ID.",
tags: [OpenAPITags.AiProvider],
request: {
- params: paramsSchema
+ params: z.object({
+ providerId: z.string()
+ })
+ },
+ responses: {
+ 200: {
+ description: "Successful response"
+ }
+ }
+});
+
+registry.registerPath({
+ method: "get",
+ path: "/org/{orgId}/ai-provider/{niceId}",
+ description:
+ "Get an AI provider by orgId and niceId. NiceId is a readable ID for the provider and unique on a per org basis.",
+ tags: [OpenAPITags.AiProvider],
+ request: {
+ params: z.object({
+ orgId: z.string(),
+ niceId: z.string()
+ })
},
responses: {
200: {
@@ -46,22 +98,22 @@ export async function getAiProvider(
);
}
- const { providerId } = parsedParams.data;
+ const { providerId, niceId, orgId } = parsedParams.data;
- const [provider] =
- req.aiProvider && req.aiProvider.providerId === providerId
- ? [req.aiProvider]
- : await db
- .select()
- .from(aiProviders)
- .where(eq(aiProviders.providerId, providerId))
- .limit(1);
+ const provider =
+ req.aiProvider &&
+ (req.aiProvider.providerId === providerId ||
+ (niceId &&
+ req.aiProvider.niceId === niceId &&
+ req.aiProvider.orgId === orgId))
+ ? req.aiProvider
+ : await query(providerId, niceId, orgId);
if (!provider) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
- `AI provider with ID ${providerId} not found`
+ `AI provider with ID ${providerId || niceId} not found`
)
);
}
diff --git a/server/routers/external.ts b/server/routers/external.ts
index 17cf0d921..e182d2b44 100644
--- a/server/routers/external.ts
+++ b/server/routers/external.ts
@@ -1589,6 +1589,13 @@ authenticated.get(
verifyUserHasAction(ActionsEnum.getAiProvider),
aiProvider.getAiProvider
);
+authenticated.get(
+ "/org/:orgId/ai-provider/:niceId",
+ verifyOrgAccess,
+ verifyAiProviderAccess,
+ verifyUserHasAction(ActionsEnum.getAiProvider),
+ aiProvider.getAiProvider
+);
authenticated.put(
"/ai-provider/:providerId/target",
diff --git a/server/routers/integration.ts b/server/routers/integration.ts
index 5dc9315e9..f609443d4 100644
--- a/server/routers/integration.ts
+++ b/server/routers/integration.ts
@@ -1624,6 +1624,13 @@ authenticated.get(
verifyApiKeyHasAction(ActionsEnum.getAiProvider),
aiProvider.getAiProvider
);
+authenticated.get(
+ "/org/:orgId/ai-provider/:niceId",
+ verifyApiKeyOrgAccess,
+ verifyApiKeyAiProviderAccess,
+ verifyApiKeyHasAction(ActionsEnum.getAiProvider),
+ aiProvider.getAiProvider
+);
authenticated.put(
"/ai-provider/:providerId/target",
diff --git a/src/app/[orgId]/settings/ai-providers/[providerId]/authentication/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/authentication/page.tsx
similarity index 100%
rename from src/app/[orgId]/settings/ai-providers/[providerId]/authentication/page.tsx
rename to src/app/[orgId]/settings/ai-providers/[niceId]/authentication/page.tsx
diff --git a/src/app/[orgId]/settings/ai-providers/[providerId]/budget/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/budget/page.tsx
similarity index 100%
rename from src/app/[orgId]/settings/ai-providers/[providerId]/budget/page.tsx
rename to src/app/[orgId]/settings/ai-providers/[niceId]/budget/page.tsx
diff --git a/src/app/[orgId]/settings/ai-providers/[niceId]/configuration/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/configuration/page.tsx
new file mode 100644
index 000000000..ee3f676ae
--- /dev/null
+++ b/src/app/[orgId]/settings/ai-providers/[niceId]/configuration/page.tsx
@@ -0,0 +1,12 @@
+import { redirect } from "next/navigation";
+
+type Props = {
+ params: Promise<{ orgId: string; niceId: string }>;
+};
+
+export default async function AiProviderConfigurationRedirect({
+ params
+}: Props) {
+ const { orgId, niceId } = await params;
+ redirect(`/${orgId}/settings/ai-providers/${niceId}/network`);
+}
diff --git a/src/app/[orgId]/settings/ai-providers/[providerId]/general/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/general/page.tsx
similarity index 85%
rename from src/app/[orgId]/settings/ai-providers/[providerId]/general/page.tsx
rename to src/app/[orgId]/settings/ai-providers/[niceId]/general/page.tsx
index 9e41ff9c3..b65b1984a 100644
--- a/src/app/[orgId]/settings/ai-providers/[providerId]/general/page.tsx
+++ b/src/app/[orgId]/settings/ai-providers/[niceId]/general/page.tsx
@@ -55,6 +55,7 @@ export default function AiProviderGeneralPage() {
.string()
.trim()
.min(1, { message: t("nameRequired") }),
+ niceId: z.string().min(1).max(255).optional(),
enabled: z.boolean(),
capabilities: z.array(z.enum(AI_CAPABILITIES)).optional()
})
@@ -76,6 +77,7 @@ export default function AiProviderGeneralPage() {
resolver: zodResolver(generalSchema),
defaultValues: {
name: provider.name,
+ niceId: provider.niceId,
enabled: provider.enabled,
capabilities: provider.capabilities ?? []
}
@@ -86,10 +88,12 @@ export default function AiProviderGeneralPage() {
try {
const body: {
name: string;
+ niceId?: string;
enabled: boolean;
capabilities?: AiCapability[];
} = {
name: values.name.trim(),
+ niceId: values.niceId,
enabled: values.enabled,
capabilities: values.capabilities ?? []
};
@@ -101,6 +105,7 @@ export default function AiProviderGeneralPage() {
updateProvider(updated);
form.reset({
name: updated.name,
+ niceId: updated.niceId,
enabled: updated.enabled,
capabilities: updated.capabilities ?? []
});
@@ -108,6 +113,13 @@ export default function AiProviderGeneralPage() {
title: t("success"),
description: t("aiProviderUpdated")
});
+
+ if (values.niceId && values.niceId !== provider.niceId) {
+ router.replace(
+ `/${provider.orgId}/settings/ai-providers/${values.niceId}/general`
+ );
+ }
+
router.refresh();
} catch (e) {
toast({
@@ -190,6 +202,29 @@ export default function AiProviderGeneralPage() {
/>
+
+ (
+
+
+ {t("identifier")}
+
+
+
+
+
+
+ )}
+ />
+
+
;
+ params: Promise<{ orgId: string; niceId: string }>;
};
export default async function AiProviderLayout({ children, params }: Props) {
- const { orgId, providerId } = await params;
+ const { orgId, niceId } = await params;
const t = await getTranslations();
let provider = null;
try {
const res = await internal.get>(
- `/ai-provider/${providerId}`,
+ `/org/${orgId}/ai-provider/${niceId}`,
await authCookieHeader()
);
provider = res.data.data.provider;
@@ -63,23 +63,23 @@ export default async function AiProviderLayout({ children, params }: Props) {
const navItems = [
{
title: t("general"),
- href: "/{orgId}/settings/ai-providers/{providerId}/general"
+ href: "/{orgId}/settings/ai-providers/{niceId}/general"
},
{
title: t("aiProviderNetworkSettings"),
- href: "/{orgId}/settings/ai-providers/{providerId}/network"
+ href: "/{orgId}/settings/ai-providers/{niceId}/network"
},
{
title: t("aiProviderModels"),
- href: "/{orgId}/settings/ai-providers/{providerId}/models"
+ href: "/{orgId}/settings/ai-providers/{niceId}/models"
},
{
title: t("aiProviderAuthSettings"),
- href: "/{orgId}/settings/ai-providers/{providerId}/authentication"
+ href: "/{orgId}/settings/ai-providers/{niceId}/authentication"
},
{
title: t("aiProviderBudgetSettings"),
- href: "/{orgId}/settings/ai-providers/{providerId}/budget"
+ href: "/{orgId}/settings/ai-providers/{niceId}/budget"
}
];
diff --git a/src/app/[orgId]/settings/ai-providers/[providerId]/models/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/models/page.tsx
similarity index 100%
rename from src/app/[orgId]/settings/ai-providers/[providerId]/models/page.tsx
rename to src/app/[orgId]/settings/ai-providers/[niceId]/models/page.tsx
diff --git a/src/app/[orgId]/settings/ai-providers/[providerId]/network/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/network/page.tsx
similarity index 100%
rename from src/app/[orgId]/settings/ai-providers/[providerId]/network/page.tsx
rename to src/app/[orgId]/settings/ai-providers/[niceId]/network/page.tsx
diff --git a/src/app/[orgId]/settings/ai-providers/[niceId]/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/page.tsx
new file mode 100644
index 000000000..664127d17
--- /dev/null
+++ b/src/app/[orgId]/settings/ai-providers/[niceId]/page.tsx
@@ -0,0 +1,10 @@
+import { redirect } from "next/navigation";
+
+type Props = {
+ params: Promise<{ orgId: string; niceId: string }>;
+};
+
+export default async function AiProviderPage({ params }: Props) {
+ const { orgId, niceId } = await params;
+ redirect(`/${orgId}/settings/ai-providers/${niceId}/general`);
+}
diff --git a/src/app/[orgId]/settings/ai-providers/[providerId]/configuration/page.tsx b/src/app/[orgId]/settings/ai-providers/[providerId]/configuration/page.tsx
deleted file mode 100644
index bf003b96e..000000000
--- a/src/app/[orgId]/settings/ai-providers/[providerId]/configuration/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { redirect } from "next/navigation";
-
-type Props = {
- params: Promise<{ orgId: string; providerId: string }>;
-};
-
-export default async function AiProviderConfigurationRedirect({
- params
-}: Props) {
- const { orgId, providerId } = await params;
- redirect(`/${orgId}/settings/ai-providers/${providerId}/network`);
-}
diff --git a/src/app/[orgId]/settings/ai-providers/[providerId]/page.tsx b/src/app/[orgId]/settings/ai-providers/[providerId]/page.tsx
deleted file mode 100644
index 2daca1227..000000000
--- a/src/app/[orgId]/settings/ai-providers/[providerId]/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { redirect } from "next/navigation";
-
-type Props = {
- params: Promise<{ orgId: string; providerId: string }>;
-};
-
-export default async function AiProviderPage({ params }: Props) {
- const { orgId, providerId } = await params;
- redirect(`/${orgId}/settings/ai-providers/${providerId}/general`);
-}
diff --git a/src/app/[orgId]/settings/ai-providers/create/page.tsx b/src/app/[orgId]/settings/ai-providers/create/page.tsx
index 7684b2d48..29f005e8a 100644
--- a/src/app/[orgId]/settings/ai-providers/create/page.tsx
+++ b/src/app/[orgId]/settings/ai-providers/create/page.tsx
@@ -222,6 +222,7 @@ export default function CreateAiProviderPage() {
>(`/org/${orgId}/ai-provider`, toAiProviderCreatePayload(values));
const providerId = res.data.data.provider.providerId;
+ const niceId = res.data.data.provider.niceId;
if (showTargets && targets.length > 0) {
try {
@@ -236,7 +237,7 @@ export default function CreateAiProviderPage() {
)
});
router.push(
- `/${orgId}/settings/ai-providers/${providerId}/network`
+ `/${orgId}/settings/ai-providers/${niceId}/network`
);
return;
}
@@ -255,7 +256,7 @@ export default function CreateAiProviderPage() {
)
});
router.push(
- `/${orgId}/settings/ai-providers/${providerId}/models`
+ `/${orgId}/settings/ai-providers/${niceId}/models`
);
return;
}
@@ -266,7 +267,7 @@ export default function CreateAiProviderPage() {
description: t("aiProviderCreated")
});
- router.push(`/${orgId}/settings/ai-providers/${providerId}`);
+ router.push(`/${orgId}/settings/ai-providers/${niceId}`);
} catch (e) {
toast({
variant: "destructive",
diff --git a/src/app/[orgId]/settings/ai-providers/page.tsx b/src/app/[orgId]/settings/ai-providers/page.tsx
index 9858641e2..024170590 100644
--- a/src/app/[orgId]/settings/ai-providers/page.tsx
+++ b/src/app/[orgId]/settings/ai-providers/page.tsx
@@ -53,6 +53,7 @@ export default async function AiProvidersPage({ params, searchParams }: Props) {
orgId={orgId}
providers={providers.map((provider) => ({
providerId: provider.providerId,
+ niceId: provider.niceId,
name: provider.name,
type: provider.type,
routingMode: provider.routingMode,
diff --git a/src/components/AiProvidersTable.tsx b/src/components/AiProvidersTable.tsx
index ba7fbb00c..82bf23cfe 100644
--- a/src/components/AiProvidersTable.tsx
+++ b/src/components/AiProvidersTable.tsx
@@ -27,6 +27,7 @@ import { useDebouncedCallback } from "use-debounce";
export type AiProviderRow = {
providerId: number;
+ niceId: string;
name: string;
type: string;
routingMode: string;
@@ -176,7 +177,7 @@ export default function AiProvidersTable({
header: () => {t("name")},
cell: ({ row }) => (
{row.original.name}
@@ -238,7 +239,7 @@ export default function AiProvidersTable({
{t("edit")}
@@ -256,7 +257,7 @@ export default function AiProvidersTable({