add niceId to provider

This commit is contained in:
Owen
2026-08-14 09:30:44 -04:00
parent ee75a09f8c
commit f0f5e9219b
5 changed files with 171 additions and 93 deletions
+27
View File
@@ -1,6 +1,7 @@
import { join } from "path"; import { join } from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { import {
aiProviders,
clients, clients,
db, db,
resourcePolicies, resourcePolicies,
@@ -113,6 +114,32 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
} }
} }
export async function getUniqueProviderName(orgId: string): Promise<string> {
let loops = 0;
while (true) {
if (loops > 100) {
throw new Error("Could not generate a unique name");
}
const name = generateName();
const aiProviderCount = await db
.select({
niceId: aiProviders.niceId,
orgId: aiProviders.orgId
})
.from(aiProviders)
.where(
and(eq(aiProviders.niceId, name), eq(aiProviders.orgId, orgId))
);
if (aiProviderCount.length === 0) {
return name;
}
loops++;
}
}
export async function getUniqueResourcePolicyName( export async function getUniqueResourcePolicyName(
orgId: string orgId: string
): Promise<string> { ): Promise<string> {
+51 -46
View File
@@ -1684,52 +1684,57 @@ export const statusHistory = pgTable(
] ]
); );
export const aiProviders = pgTable("aiProviders", { export const aiProviders = pgTable(
providerId: serial("providerId").primaryKey(), "aiProviders",
orgId: varchar("orgId") {
.notNull() providerId: serial("providerId").primaryKey(),
.references(() => orgs.orgId, { onDelete: "cascade" }), orgId: varchar("orgId")
name: varchar("name").notNull(), .notNull()
type: varchar("type") .references(() => orgs.orgId, { onDelete: "cascade" }),
.$type< name: varchar("name").notNull(),
| "openai" niceId: varchar("niceId").notNull(),
| "anthropic" type: varchar("type")
| "googleGemini" .$type<
| "vertexAi" | "openai"
| "bedrock" | "anthropic"
| "microsoftFoundry" | "googleGemini"
| "openRouter" | "vertexAi"
| "vercelAiGateway" | "bedrock"
| "custom" | "microsoftFoundry"
>() | "openRouter"
.notNull(), | "vercelAiGateway"
upstreamUrl: text("upstreamUrl"), | "custom"
apiKey: text("apiKey"), >()
apiKeyLastChars: varchar("apiKeyLastChars"), .notNull(),
authType: varchar("authType") upstreamUrl: text("upstreamUrl"),
.$type< apiKey: text("apiKey"),
| "bearer" apiKeyLastChars: varchar("apiKeyLastChars"),
| "x-api-key" authType: varchar("authType")
| "x-goog-api-key" .$type<
| "hec" | "bearer"
| "cf-aig-authorization" | "x-api-key"
| "none" | "x-goog-api-key"
| "passthrough" | "hec"
>() | "cf-aig-authorization"
.notNull(), | "none"
routingMode: varchar("routingMode") | "passthrough"
.$type<"url" | "target">() >()
.notNull() .notNull(),
.default("url"), routingMode: varchar("routingMode")
capabilities: text("capabilities").notNull().default("[]"), .$type<"url" | "target">()
headers: text("headers"), // JSON array of { name, value } .notNull()
skipTlsVerification: boolean("skipTlsVerification") .default("url"),
.notNull() capabilities: text("capabilities").notNull().default("[]"),
.default(false), headers: text("headers"), // JSON array of { name, value }
enabled: boolean("enabled").notNull().default(true), skipTlsVerification: boolean("skipTlsVerification")
createdAt: bigint("createdAt", { mode: "number" }).notNull(), .notNull()
updatedAt: bigint("updatedAt", { mode: "number" }).notNull() .default(false),
}); enabled: boolean("enabled").notNull().default(true),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
},
(t) => [index("idx_aiProviders_orgId_niceId").on(t.orgId, t.niceId)]
);
export const aiModels = pgTable( export const aiModels = pgTable(
"aiModels", "aiModels",
+53 -46
View File
@@ -1668,52 +1668,59 @@ export const statusHistory = sqliteTable(
] ]
); );
export const aiProviders = sqliteTable("aiProviders", { export const aiProviders = sqliteTable(
providerId: integer("providerId").primaryKey({ autoIncrement: true }), "aiProviders",
orgId: text("orgId") {
.notNull() providerId: integer("providerId").primaryKey({ autoIncrement: true }),
.references(() => orgs.orgId, { onDelete: "cascade" }), orgId: text("orgId")
name: text("name").notNull(), .notNull()
type: text("type") .references(() => orgs.orgId, { onDelete: "cascade" }),
.$type< name: text("name").notNull(),
| "openai" niceId: text("niceId").notNull(),
| "anthropic" type: text("type")
| "googleGemini" .$type<
| "vertexAi" | "openai"
| "bedrock" | "anthropic"
| "microsoftFoundry" | "googleGemini"
| "openRouter" | "vertexAi"
| "vercelAiGateway" | "bedrock"
| "custom" | "microsoftFoundry"
>() | "openRouter"
.notNull(), | "vercelAiGateway"
upstreamUrl: text("upstreamUrl"), | "custom"
apiKey: text("apiKey"), >()
apiKeyLastChars: text("apiKeyLastChars"), .notNull(),
authType: text("authType") upstreamUrl: text("upstreamUrl"),
.$type< apiKey: text("apiKey"),
| "bearer" apiKeyLastChars: text("apiKeyLastChars"),
| "x-api-key" authType: text("authType")
| "x-goog-api-key" .$type<
| "hec" | "bearer"
| "cf-aig-authorization" | "x-api-key"
| "none" | "x-goog-api-key"
| "passthrough" | "hec"
>() | "cf-aig-authorization"
.notNull(), | "none"
routingMode: text("routingMode") | "passthrough"
.$type<"url" | "target">() >()
.notNull() .notNull(),
.default("url"), routingMode: text("routingMode")
capabilities: text("capabilities").notNull().default("[]"), .$type<"url" | "target">()
headers: text("headers"), // JSON array of { name, value } .notNull()
skipTlsVerification: integer("skipTlsVerification", { mode: "boolean" }) .default("url"),
.notNull() capabilities: text("capabilities").notNull().default("[]"),
.default(false), headers: text("headers"), // JSON array of { name, value }
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), skipTlsVerification: integer("skipTlsVerification", { mode: "boolean" })
createdAt: integer("createdAt").notNull(), .notNull()
updatedAt: integer("updatedAt").notNull() .default(false),
}); enabled: integer("enabled", { mode: "boolean" })
.notNull()
.default(true),
createdAt: integer("createdAt").notNull(),
updatedAt: integer("updatedAt").notNull()
},
(t) => [index("idx_aiProviders_orgId_niceId").on(t.orgId, t.niceId)]
);
export const aiModels = sqliteTable( export const aiModels = sqliteTable(
"aiModels", "aiModels",
@@ -25,6 +25,7 @@ import {
refineProviderUpstreamFields refineProviderUpstreamFields
} from "@server/routers/aiProvider/validation"; } from "@server/routers/aiProvider/validation";
import { serializeCapabilities } from "@server/lib/aiCapabilities"; import { serializeCapabilities } from "@server/lib/aiCapabilities";
import { getUniqueProviderName, getUniqueResourceName } from "@server/db/names";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string().nonempty() orgId: z.string().nonempty()
@@ -133,11 +134,14 @@ export async function createAiProvider(
); );
} }
const niceId = await getUniqueProviderName(orgId);
const [provider] = await db const [provider] = await db
.insert(aiProviders) .insert(aiProviders)
.values({ .values({
orgId, orgId,
name, name,
niceId,
type, type,
upstreamUrl: resolved.upstreamUrl, upstreamUrl: resolved.upstreamUrl,
apiKey: encryptedApiKey, apiKey: encryptedApiKey,
+36 -1
View File
@@ -7,7 +7,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { eq } from "drizzle-orm"; import { eq, ne, and } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto"; import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types"; import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
@@ -37,6 +37,15 @@ const paramsSchema = z.strictObject({
const bodySchema = z.strictObject({ const bodySchema = z.strictObject({
name: z.string().nonempty().optional(), name: z.string().nonempty().optional(),
niceId: z
.string()
.min(1)
.max(255)
.regex(
/^[a-zA-Z0-9-]+$/,
"niceId can only contain letters, numbers, and dashes"
)
.optional(),
upstreamUrl: z.url().optional().nullable(), upstreamUrl: z.url().optional().nullable(),
apiKey: z.string().optional(), apiKey: z.string().optional(),
authType: aiAuthTypeSchema.optional(), authType: aiAuthTypeSchema.optional(),
@@ -170,6 +179,9 @@ export async function updateAiProvider(
if (body.name !== undefined) { if (body.name !== undefined) {
updateData.name = body.name; updateData.name = body.name;
} }
if (body.niceId !== undefined) {
updateData.niceId = body.niceId;
}
if (body.skipTlsVerification !== undefined) { if (body.skipTlsVerification !== undefined) {
updateData.skipTlsVerification = body.skipTlsVerification; updateData.skipTlsVerification = body.skipTlsVerification;
} }
@@ -199,6 +211,29 @@ export async function updateAiProvider(
updateData.headers = serializeAiProviderHeaders(body.headers, key); updateData.headers = serializeAiProviderHeaders(body.headers, key);
} }
if (updateData.niceId) {
const [existingAiProvider] = await db
.select()
.from(aiProviders)
.where(
and(
eq(aiProviders.niceId, updateData.niceId),
eq(aiProviders.orgId, existing.orgId),
ne(aiProviders.providerId, existing.providerId) // exclude the current provider from the search
)
)
.limit(1);
if (existingAiProvider) {
return next(
createHttpError(
HttpCode.CONFLICT,
`A resource with niceId "${updateData.niceId}" already exists`
)
);
}
}
const [provider] = await db const [provider] = await db
.update(aiProviders) .update(aiProviders)
.set(updateData) .set(updateData)