From f0f5e9219beeea3382ccb1462e61e8452538c9cb Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 14 Aug 2026 09:30:44 -0400 Subject: [PATCH] add niceId to provider --- server/db/names.ts | 27 +++++ server/db/pg/schema/schema.ts | 97 +++++++++--------- server/db/sqlite/schema/schema.ts | 99 ++++++++++--------- server/routers/aiProvider/createAiProvider.ts | 4 + server/routers/aiProvider/updateAiProvider.ts | 37 ++++++- 5 files changed, 171 insertions(+), 93 deletions(-) diff --git a/server/db/names.ts b/server/db/names.ts index ebe38573d..37e851379 100644 --- a/server/db/names.ts +++ b/server/db/names.ts @@ -1,6 +1,7 @@ import { join } from "path"; import { readFileSync } from "fs"; import { + aiProviders, clients, db, resourcePolicies, @@ -113,6 +114,32 @@ export async function getUniqueResourceName(orgId: string): Promise { } } +export async function getUniqueProviderName(orgId: string): Promise { + 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( orgId: string ): Promise { diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 50fa3753b..432afbbc1 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -1684,52 +1684,57 @@ export const statusHistory = pgTable( ] ); -export const aiProviders = pgTable("aiProviders", { - providerId: serial("providerId").primaryKey(), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - name: varchar("name").notNull(), - type: varchar("type") - .$type< - | "openai" - | "anthropic" - | "googleGemini" - | "vertexAi" - | "bedrock" - | "microsoftFoundry" - | "openRouter" - | "vercelAiGateway" - | "custom" - >() - .notNull(), - upstreamUrl: text("upstreamUrl"), - apiKey: text("apiKey"), - apiKeyLastChars: varchar("apiKeyLastChars"), - authType: varchar("authType") - .$type< - | "bearer" - | "x-api-key" - | "x-goog-api-key" - | "hec" - | "cf-aig-authorization" - | "none" - | "passthrough" - >() - .notNull(), - routingMode: varchar("routingMode") - .$type<"url" | "target">() - .notNull() - .default("url"), - capabilities: text("capabilities").notNull().default("[]"), - headers: text("headers"), // JSON array of { name, value } - skipTlsVerification: boolean("skipTlsVerification") - .notNull() - .default(false), - enabled: boolean("enabled").notNull().default(true), - createdAt: bigint("createdAt", { mode: "number" }).notNull(), - updatedAt: bigint("updatedAt", { mode: "number" }).notNull() -}); +export const aiProviders = pgTable( + "aiProviders", + { + providerId: serial("providerId").primaryKey(), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + name: varchar("name").notNull(), + niceId: varchar("niceId").notNull(), + type: varchar("type") + .$type< + | "openai" + | "anthropic" + | "googleGemini" + | "vertexAi" + | "bedrock" + | "microsoftFoundry" + | "openRouter" + | "vercelAiGateway" + | "custom" + >() + .notNull(), + upstreamUrl: text("upstreamUrl"), + apiKey: text("apiKey"), + apiKeyLastChars: varchar("apiKeyLastChars"), + authType: varchar("authType") + .$type< + | "bearer" + | "x-api-key" + | "x-goog-api-key" + | "hec" + | "cf-aig-authorization" + | "none" + | "passthrough" + >() + .notNull(), + routingMode: varchar("routingMode") + .$type<"url" | "target">() + .notNull() + .default("url"), + capabilities: text("capabilities").notNull().default("[]"), + headers: text("headers"), // JSON array of { name, value } + skipTlsVerification: boolean("skipTlsVerification") + .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( "aiModels", diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 628563ee3..c70424152 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1668,52 +1668,59 @@ export const statusHistory = sqliteTable( ] ); -export const aiProviders = sqliteTable("aiProviders", { - providerId: integer("providerId").primaryKey({ autoIncrement: true }), - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - name: text("name").notNull(), - type: text("type") - .$type< - | "openai" - | "anthropic" - | "googleGemini" - | "vertexAi" - | "bedrock" - | "microsoftFoundry" - | "openRouter" - | "vercelAiGateway" - | "custom" - >() - .notNull(), - upstreamUrl: text("upstreamUrl"), - apiKey: text("apiKey"), - apiKeyLastChars: text("apiKeyLastChars"), - authType: text("authType") - .$type< - | "bearer" - | "x-api-key" - | "x-goog-api-key" - | "hec" - | "cf-aig-authorization" - | "none" - | "passthrough" - >() - .notNull(), - routingMode: text("routingMode") - .$type<"url" | "target">() - .notNull() - .default("url"), - capabilities: text("capabilities").notNull().default("[]"), - headers: text("headers"), // JSON array of { name, value } - skipTlsVerification: integer("skipTlsVerification", { mode: "boolean" }) - .notNull() - .default(false), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - createdAt: integer("createdAt").notNull(), - updatedAt: integer("updatedAt").notNull() -}); +export const aiProviders = sqliteTable( + "aiProviders", + { + providerId: integer("providerId").primaryKey({ autoIncrement: true }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + name: text("name").notNull(), + niceId: text("niceId").notNull(), + type: text("type") + .$type< + | "openai" + | "anthropic" + | "googleGemini" + | "vertexAi" + | "bedrock" + | "microsoftFoundry" + | "openRouter" + | "vercelAiGateway" + | "custom" + >() + .notNull(), + upstreamUrl: text("upstreamUrl"), + apiKey: text("apiKey"), + apiKeyLastChars: text("apiKeyLastChars"), + authType: text("authType") + .$type< + | "bearer" + | "x-api-key" + | "x-goog-api-key" + | "hec" + | "cf-aig-authorization" + | "none" + | "passthrough" + >() + .notNull(), + routingMode: text("routingMode") + .$type<"url" | "target">() + .notNull() + .default("url"), + capabilities: text("capabilities").notNull().default("[]"), + headers: text("headers"), // JSON array of { name, value } + skipTlsVerification: integer("skipTlsVerification", { mode: "boolean" }) + .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( "aiModels", diff --git a/server/routers/aiProvider/createAiProvider.ts b/server/routers/aiProvider/createAiProvider.ts index f115d674a..8a6ad9e4d 100644 --- a/server/routers/aiProvider/createAiProvider.ts +++ b/server/routers/aiProvider/createAiProvider.ts @@ -25,6 +25,7 @@ import { refineProviderUpstreamFields } from "@server/routers/aiProvider/validation"; import { serializeCapabilities } from "@server/lib/aiCapabilities"; +import { getUniqueProviderName, getUniqueResourceName } from "@server/db/names"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() @@ -133,11 +134,14 @@ export async function createAiProvider( ); } + const niceId = await getUniqueProviderName(orgId); + const [provider] = await db .insert(aiProviders) .values({ orgId, name, + niceId, type, upstreamUrl: resolved.upstreamUrl, apiKey: encryptedApiKey, diff --git a/server/routers/aiProvider/updateAiProvider.ts b/server/routers/aiProvider/updateAiProvider.ts index 1f22dc6ef..19555dda9 100644 --- a/server/routers/aiProvider/updateAiProvider.ts +++ b/server/routers/aiProvider/updateAiProvider.ts @@ -7,7 +7,7 @@ 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 { eq, ne, and } from "drizzle-orm"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types"; @@ -37,6 +37,15 @@ const paramsSchema = z.strictObject({ const bodySchema = z.strictObject({ 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(), apiKey: z.string().optional(), authType: aiAuthTypeSchema.optional(), @@ -170,6 +179,9 @@ export async function updateAiProvider( if (body.name !== undefined) { updateData.name = body.name; } + if (body.niceId !== undefined) { + updateData.niceId = body.niceId; + } if (body.skipTlsVerification !== undefined) { updateData.skipTlsVerification = body.skipTlsVerification; } @@ -199,6 +211,29 @@ export async function updateAiProvider( 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 .update(aiProviders) .set(updateData)