diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 83f1e19bd..2f8199aa8 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -1554,7 +1554,17 @@ export const aiProviders = pgTable("aiProviders", { .references(() => orgs.orgId, { onDelete: "cascade" }), name: varchar("name").notNull(), type: varchar("type") - .$type<"openai" | "anthropic" | "bedrock" | "custom">() + .$type< + | "openai" + | "anthropic" + | "googleGemini" + | "vertexAi" + | "bedrock" + | "microsoftFoundry" + | "openRouter" + | "vercelAiGateway" + | "custom" + >() .notNull(), upstreamUrl: text("upstreamUrl"), apiKey: text("apiKey"), diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 97059d3cb..4cc533475 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1545,7 +1545,17 @@ export const aiProviders = sqliteTable("aiProviders", { .references(() => orgs.orgId, { onDelete: "cascade" }), name: text("name").notNull(), type: text("type") - .$type<"openai" | "anthropic" | "bedrock" | "custom">() + .$type< + | "openai" + | "anthropic" + | "googleGemini" + | "vertexAi" + | "bedrock" + | "microsoftFoundry" + | "openRouter" + | "vercelAiGateway" + | "custom" + >() .notNull(), upstreamUrl: text("upstreamUrl"), apiKey: text("apiKey"), diff --git a/server/lib/aiProviderDefaults.ts b/server/lib/aiProviderDefaults.ts index d89be99a0..63c060b62 100644 --- a/server/lib/aiProviderDefaults.ts +++ b/server/lib/aiProviderDefaults.ts @@ -1,9 +1,19 @@ -export type AiProviderType = "openai" | "anthropic" | "bedrock" | "custom"; +export type AiProviderType = + | "openai" + | "anthropic" + | "googleGemini" + | "vertexAi" + | "bedrock" + | "microsoftFoundry" + | "openRouter" + | "vercelAiGateway" + | "custom"; + export type AiProviderAuthType = "bearer"; export type AiBudgetUnit = "usd" | "tokens"; type AiProviderDefaults = { - upstreamUrl: string; + upstreamUrl: string | null; authType: AiProviderAuthType; }; @@ -19,12 +29,39 @@ export const AI_PROVIDER_DEFAULTS: Record< upstreamUrl: "https://api.anthropic.com", authType: "bearer" }, + googleGemini: { + upstreamUrl: "https://generativelanguage.googleapis.com/v1beta/openai/", + authType: "bearer" + }, + vertexAi: { + upstreamUrl: null, + authType: "bearer" + }, bedrock: { upstreamUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", authType: "bearer" + }, + microsoftFoundry: { + upstreamUrl: null, + authType: "bearer" + }, + openRouter: { + upstreamUrl: "https://openrouter.ai/api/v1", + authType: "bearer" + }, + vercelAiGateway: { + upstreamUrl: "https://ai-gateway.vercel.sh/v1", + authType: "bearer" } }; +export function providerRequiresUpstreamUrl(type: AiProviderType): boolean { + if (type === "custom") { + return true; + } + return AI_PROVIDER_DEFAULTS[type].upstreamUrl === null; +} + export function resolveAiProviderConfig(input: { type: AiProviderType; upstreamUrl: string | null; diff --git a/server/routers/aiProvider/createAiProvider.ts b/server/routers/aiProvider/createAiProvider.ts index dad11d18d..27420bd34 100644 --- a/server/routers/aiProvider/createAiProvider.ts +++ b/server/routers/aiProvider/createAiProvider.ts @@ -16,7 +16,7 @@ import { aiBudgetUnitSchema, aiProviderTypeSchema, refineBudgetFields, - refineCustomProviderFields + refineProviderUpstreamFields } from "@server/routers/aiProvider/validation"; const paramsSchema = z.strictObject({ @@ -36,7 +36,7 @@ const bodySchema = z enabled: z.boolean().optional() }) .superRefine((data, ctx) => { - refineCustomProviderFields(data, ctx); + refineProviderUpstreamFields(data, ctx); refineBudgetFields(data, ctx); }); diff --git a/server/routers/aiProvider/updateAiProvider.ts b/server/routers/aiProvider/updateAiProvider.ts index 8525e1dd3..186b5bcd8 100644 --- a/server/routers/aiProvider/updateAiProvider.ts +++ b/server/routers/aiProvider/updateAiProvider.ts @@ -12,13 +12,17 @@ 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, + aiProviderTypeSchema, refineBudgetFields, - refineCustomProviderFields + refineProviderUpstreamFields } from "@server/routers/aiProvider/validation"; +import { + providerRequiresUpstreamUrl, + type AiProviderType +} from "@server/lib/aiProviderDefaults"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty(), @@ -115,26 +119,29 @@ export async function updateAiProvider( } const providerType = existing.type as AiProviderType; + const nextUpstreamUrl = + body.upstreamUrl !== undefined + ? body.upstreamUrl + : existing.upstreamUrl; + const nextAuthType = + body.authType !== undefined ? body.authType : existing.authType; - if (providerType === "custom") { - const nextUpstreamUrl = - body.upstreamUrl !== undefined - ? body.upstreamUrl - : existing.upstreamUrl; - const nextAuthType = - body.authType !== undefined ? body.authType : existing.authType; - + if ( + providerRequiresUpstreamUrl(providerType) || + body.upstreamUrl !== undefined || + body.authType !== undefined + ) { const validation = z .object({ - type: z.literal("custom"), + type: aiProviderTypeSchema, upstreamUrl: z.string().nullable().optional(), authType: aiAuthTypeSchema.nullable().optional() }) .superRefine((data, ctx) => - refineCustomProviderFields(data, ctx) + refineProviderUpstreamFields(data, ctx) ) .safeParse({ - type: "custom", + type: providerType, upstreamUrl: nextUpstreamUrl, authType: nextAuthType }); diff --git a/server/routers/aiProvider/validation.ts b/server/routers/aiProvider/validation.ts index 62b442c59..ab70800b5 100644 --- a/server/routers/aiProvider/validation.ts +++ b/server/routers/aiProvider/validation.ts @@ -1,13 +1,19 @@ import { z } from "zod"; -import type { - AiBudgetUnit, - AiProviderType +import { + providerRequiresUpstreamUrl, + type AiBudgetUnit, + type AiProviderType } from "@server/lib/aiProviderDefaults"; export const aiProviderTypeSchema = z.enum([ "openai", "anthropic", + "googleGemini", + "vertexAi", "bedrock", + "microsoftFoundry", + "openRouter", + "vercelAiGateway", "custom" ]); @@ -36,7 +42,7 @@ export function refineBudgetFields( } } -export function refineCustomProviderFields( +export function refineProviderUpstreamFields( data: { type: AiProviderType; upstreamUrl?: string | null; @@ -44,19 +50,15 @@ export function refineCustomProviderFields( }, ctx: z.RefinementCtx ) { - if (data.type !== "custom") { - return; - } - - if (!data.upstreamUrl) { + if (providerRequiresUpstreamUrl(data.type) && !data.upstreamUrl) { ctx.addIssue({ code: "custom", - message: "upstreamUrl is required for custom providers", + message: `upstreamUrl is required for ${data.type} providers`, path: ["upstreamUrl"] }); } - if (!data.authType) { + if (data.type === "custom" && !data.authType) { ctx.addIssue({ code: "custom", message: "authType is required for custom providers",