add default providers and support overrides

This commit is contained in:
miloschwartz
2026-07-31 15:55:10 -04:00
parent f1a2da277e
commit 1333c8e841
6 changed files with 96 additions and 30 deletions
+11 -1
View File
@@ -1554,7 +1554,17 @@ export const aiProviders = pgTable("aiProviders", {
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
name: varchar("name").notNull(), name: varchar("name").notNull(),
type: varchar("type") type: varchar("type")
.$type<"openai" | "anthropic" | "bedrock" | "custom">() .$type<
| "openai"
| "anthropic"
| "googleGemini"
| "vertexAi"
| "bedrock"
| "microsoftFoundry"
| "openRouter"
| "vercelAiGateway"
| "custom"
>()
.notNull(), .notNull(),
upstreamUrl: text("upstreamUrl"), upstreamUrl: text("upstreamUrl"),
apiKey: text("apiKey"), apiKey: text("apiKey"),
+11 -1
View File
@@ -1545,7 +1545,17 @@ export const aiProviders = sqliteTable("aiProviders", {
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
name: text("name").notNull(), name: text("name").notNull(),
type: text("type") type: text("type")
.$type<"openai" | "anthropic" | "bedrock" | "custom">() .$type<
| "openai"
| "anthropic"
| "googleGemini"
| "vertexAi"
| "bedrock"
| "microsoftFoundry"
| "openRouter"
| "vercelAiGateway"
| "custom"
>()
.notNull(), .notNull(),
upstreamUrl: text("upstreamUrl"), upstreamUrl: text("upstreamUrl"),
apiKey: text("apiKey"), apiKey: text("apiKey"),
+39 -2
View File
@@ -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 AiProviderAuthType = "bearer";
export type AiBudgetUnit = "usd" | "tokens"; export type AiBudgetUnit = "usd" | "tokens";
type AiProviderDefaults = { type AiProviderDefaults = {
upstreamUrl: string; upstreamUrl: string | null;
authType: AiProviderAuthType; authType: AiProviderAuthType;
}; };
@@ -19,12 +29,39 @@ export const AI_PROVIDER_DEFAULTS: Record<
upstreamUrl: "https://api.anthropic.com", upstreamUrl: "https://api.anthropic.com",
authType: "bearer" authType: "bearer"
}, },
googleGemini: {
upstreamUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
authType: "bearer"
},
vertexAi: {
upstreamUrl: null,
authType: "bearer"
},
bedrock: { bedrock: {
upstreamUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", upstreamUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
authType: "bearer" 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: { export function resolveAiProviderConfig(input: {
type: AiProviderType; type: AiProviderType;
upstreamUrl: string | null; upstreamUrl: string | null;
@@ -16,7 +16,7 @@ import {
aiBudgetUnitSchema, aiBudgetUnitSchema,
aiProviderTypeSchema, aiProviderTypeSchema,
refineBudgetFields, refineBudgetFields,
refineCustomProviderFields refineProviderUpstreamFields
} from "@server/routers/aiProvider/validation"; } from "@server/routers/aiProvider/validation";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
@@ -36,7 +36,7 @@ const bodySchema = z
enabled: z.boolean().optional() enabled: z.boolean().optional()
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
refineCustomProviderFields(data, ctx); refineProviderUpstreamFields(data, ctx);
refineBudgetFields(data, ctx); refineBudgetFields(data, ctx);
}); });
+20 -13
View File
@@ -12,13 +12,17 @@ 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";
import { toPublicAiProvider } from "@server/routers/aiProvider/types"; import { toPublicAiProvider } from "@server/routers/aiProvider/types";
import type { AiProviderType } from "@server/lib/aiProviderDefaults";
import { import {
aiAuthTypeSchema, aiAuthTypeSchema,
aiBudgetUnitSchema, aiBudgetUnitSchema,
aiProviderTypeSchema,
refineBudgetFields, refineBudgetFields,
refineCustomProviderFields refineProviderUpstreamFields
} from "@server/routers/aiProvider/validation"; } from "@server/routers/aiProvider/validation";
import {
providerRequiresUpstreamUrl,
type AiProviderType
} from "@server/lib/aiProviderDefaults";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string().nonempty(), orgId: z.string().nonempty(),
@@ -115,26 +119,29 @@ export async function updateAiProvider(
} }
const providerType = existing.type as AiProviderType; 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") { if (
const nextUpstreamUrl = providerRequiresUpstreamUrl(providerType) ||
body.upstreamUrl !== undefined body.upstreamUrl !== undefined ||
? body.upstreamUrl body.authType !== undefined
: existing.upstreamUrl; ) {
const nextAuthType =
body.authType !== undefined ? body.authType : existing.authType;
const validation = z const validation = z
.object({ .object({
type: z.literal("custom"), type: aiProviderTypeSchema,
upstreamUrl: z.string().nullable().optional(), upstreamUrl: z.string().nullable().optional(),
authType: aiAuthTypeSchema.nullable().optional() authType: aiAuthTypeSchema.nullable().optional()
}) })
.superRefine((data, ctx) => .superRefine((data, ctx) =>
refineCustomProviderFields(data, ctx) refineProviderUpstreamFields(data, ctx)
) )
.safeParse({ .safeParse({
type: "custom", type: providerType,
upstreamUrl: nextUpstreamUrl, upstreamUrl: nextUpstreamUrl,
authType: nextAuthType authType: nextAuthType
}); });
+13 -11
View File
@@ -1,13 +1,19 @@
import { z } from "zod"; import { z } from "zod";
import type { import {
AiBudgetUnit, providerRequiresUpstreamUrl,
AiProviderType type AiBudgetUnit,
type AiProviderType
} from "@server/lib/aiProviderDefaults"; } from "@server/lib/aiProviderDefaults";
export const aiProviderTypeSchema = z.enum([ export const aiProviderTypeSchema = z.enum([
"openai", "openai",
"anthropic", "anthropic",
"googleGemini",
"vertexAi",
"bedrock", "bedrock",
"microsoftFoundry",
"openRouter",
"vercelAiGateway",
"custom" "custom"
]); ]);
@@ -36,7 +42,7 @@ export function refineBudgetFields(
} }
} }
export function refineCustomProviderFields( export function refineProviderUpstreamFields(
data: { data: {
type: AiProviderType; type: AiProviderType;
upstreamUrl?: string | null; upstreamUrl?: string | null;
@@ -44,19 +50,15 @@ export function refineCustomProviderFields(
}, },
ctx: z.RefinementCtx ctx: z.RefinementCtx
) { ) {
if (data.type !== "custom") { if (providerRequiresUpstreamUrl(data.type) && !data.upstreamUrl) {
return;
}
if (!data.upstreamUrl) {
ctx.addIssue({ ctx.addIssue({
code: "custom", code: "custom",
message: "upstreamUrl is required for custom providers", message: `upstreamUrl is required for ${data.type} providers`,
path: ["upstreamUrl"] path: ["upstreamUrl"]
}); });
} }
if (!data.authType) { if (data.type === "custom" && !data.authType) {
ctx.addIssue({ ctx.addIssue({
code: "custom", code: "custom",
message: "authType is required for custom providers", message: "authType is required for custom providers",