add provider specific auth modes

This commit is contained in:
miloschwartz
2026-08-05 15:17:50 -04:00
parent c673dce484
commit 2e9bd50172
13 changed files with 178 additions and 78 deletions
+9 -1
View File
@@ -1644,7 +1644,15 @@ export const aiProviders = pgTable("aiProviders", {
upstreamUrl: text("upstreamUrl"),
apiKey: text("apiKey"),
apiKeyLastChars: varchar("apiKeyLastChars"),
authType: varchar("authType").$type<"bearer">(),
authType: varchar("authType")
.$type<
| "bearer"
| "x-api-key"
| "x-goog-api-key"
| "hec"
| "cf-aig-authorization"
>()
.notNull(),
routingMode: varchar("routingMode")
.$type<"url" | "target">()
.notNull()
+9 -1
View File
@@ -1626,7 +1626,15 @@ export const aiProviders = sqliteTable("aiProviders", {
upstreamUrl: text("upstreamUrl"),
apiKey: text("apiKey"),
apiKeyLastChars: text("apiKeyLastChars"),
authType: text("authType").$type<"bearer">(),
authType: text("authType")
.$type<
| "bearer"
| "x-api-key"
| "x-goog-api-key"
| "hec"
| "cf-aig-authorization"
>()
.notNull(),
routingMode: text("routingMode")
.$type<"url" | "target">()
.notNull()
+63 -11
View File
@@ -9,7 +9,15 @@ export type AiProviderType =
| "vercelAiGateway"
| "custom";
export type AiProviderAuthType = "bearer";
export const AI_PROVIDER_AUTH_TYPES = [
"bearer",
"x-api-key",
"x-goog-api-key",
"hec",
"cf-aig-authorization"
] as const;
export type AiProviderAuthType = (typeof AI_PROVIDER_AUTH_TYPES)[number];
export type AiBudgetUnit = "usd" | "tokens";
export type AiProviderRoutingMode = "url" | "target";
@@ -28,11 +36,11 @@ export const AI_PROVIDER_DEFAULTS: Record<
},
anthropic: {
upstreamUrl: "https://api.anthropic.com",
authType: "bearer"
authType: "x-api-key"
},
googleGemini: {
upstreamUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
authType: "bearer"
authType: "x-goog-api-key"
},
vertexAi: {
upstreamUrl: null,
@@ -56,6 +64,13 @@ export const AI_PROVIDER_DEFAULTS: Record<
}
};
const CONFLICTING_AUTH_HEADERS = [
"authorization",
"x-api-key",
"x-goog-api-key",
"cf-aig-authorization"
] as const;
export function providerRequiresUpstreamUrl(
type: AiProviderType,
routingMode: AiProviderRoutingMode = "url"
@@ -69,17 +84,18 @@ export function providerRequiresUpstreamUrl(
return AI_PROVIDER_DEFAULTS[type].upstreamUrl === null;
}
export function resolveAiProviderConfig(input: {
export function resolveAiProviderCreateFields(input: {
type: AiProviderType;
upstreamUrl: string | null;
authType: AiProviderAuthType | null;
upstreamUrl?: string | null;
authType?: AiProviderAuthType | null;
routingMode?: AiProviderRoutingMode | null;
}): {
upstreamUrl: string | null;
authType: AiProviderAuthType | null;
authType: AiProviderAuthType;
routingMode: AiProviderRoutingMode;
} {
const routingMode = input.routingMode ?? "url";
const routingMode =
input.type === "custom" ? (input.routingMode ?? "url") : "url";
if (routingMode === "target") {
return {
@@ -91,8 +107,8 @@ export function resolveAiProviderConfig(input: {
if (input.type === "custom") {
return {
upstreamUrl: input.upstreamUrl,
authType: input.authType,
upstreamUrl: input.upstreamUrl ?? null,
authType: input.authType ?? "bearer",
routingMode
};
}
@@ -100,7 +116,43 @@ export function resolveAiProviderConfig(input: {
const defaults = AI_PROVIDER_DEFAULTS[input.type];
return {
upstreamUrl: input.upstreamUrl ?? defaults.upstreamUrl,
authType: input.authType ?? defaults.authType,
authType: defaults.authType,
routingMode
};
}
/**
* Strip inbound client auth headers, then set the provider auth header
* for the given authType.
*/
export function applyAiProviderAuthHeaders(
headers: Record<string, string>,
authType: AiProviderAuthType,
apiKey: string
): void {
for (const name of CONFLICTING_AUTH_HEADERS) {
for (const key of Object.keys(headers)) {
if (key.toLowerCase() === name) {
delete headers[key];
}
}
}
switch (authType) {
case "bearer":
headers["Authorization"] = `Bearer ${apiKey}`;
break;
case "x-api-key":
headers["x-api-key"] = apiKey;
break;
case "x-goog-api-key":
headers["x-goog-api-key"] = apiKey;
break;
case "hec":
headers["Authorization"] = `Splunk ${apiKey}`;
break;
case "cf-aig-authorization":
headers["cf-aig-authorization"] = `Bearer ${apiKey}`;
break;
}
}
+4 -11
View File
@@ -19,9 +19,7 @@ import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import {
AiProviderAuthType,
AiProviderRoutingMode,
AiProviderType,
resolveAiProviderConfig
applyAiProviderAuthHeaders
} from "@server/lib/aiProviderDefaults";
import {
SESSION_COOKIE_NAME,
@@ -499,12 +497,8 @@ export async function chatCompletions(
const secret = config.getRawConfig().server.secret!;
const apiKey = decrypt(provider.apiKey, secret);
const { upstreamUrl, authType } = resolveAiProviderConfig({
type: provider.type as AiProviderType,
upstreamUrl: provider.upstreamUrl,
authType: provider.authType as AiProviderAuthType | null,
routingMode: provider.routingMode as AiProviderRoutingMode | null
});
const upstreamUrl = provider.upstreamUrl;
const authType = provider.authType as AiProviderAuthType;
if (!upstreamUrl) {
return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
@@ -541,8 +535,7 @@ export async function chatCompletions(
}
headers[key] = Array.isArray(value) ? value.join(", ") : value;
}
// TODO: temporary hardcoded auth for testing; restore bearer from authType
headers["x-api-key"] = apiKey;
applyAiProviderAuthHeaders(headers, authType, apiKey);
// No dedicated per-request TLS agent is wired up (no extra deps for
// this v1 gateway) - toggle the process-wide Node TLS check instead.
+11 -9
View File
@@ -9,6 +9,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { resolveAiProviderCreateFields } from "@server/lib/aiProviderDefaults";
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
import { toPublicAiProvider } from "@server/routers/aiProvider/types";
import {
@@ -28,7 +29,7 @@ const bodySchema = z
type: aiProviderTypeSchema,
upstreamUrl: z.url().optional().nullable(),
apiKey: z.string().optional(),
authType: aiAuthTypeSchema.optional().nullable(),
authType: aiAuthTypeSchema.optional(),
routingMode: aiRoutingModeSchema.optional(),
skipTlsVerification: z.boolean().optional(),
enabled: z.boolean().optional()
@@ -101,8 +102,12 @@ export async function createAiProvider(
const encryptedApiKey = apiKey ? encrypt(apiKey, key) : null;
const apiKeyLastChars = apiKey ? apiKey.slice(-4) : null;
const now = Date.now();
const resolvedRoutingMode =
type === "custom" ? (routingMode ?? "url") : "url";
const resolved = resolveAiProviderCreateFields({
type,
upstreamUrl,
authType,
routingMode
});
const [provider] = await db
.insert(aiProviders)
@@ -110,14 +115,11 @@ export async function createAiProvider(
orgId,
name,
type,
upstreamUrl:
resolvedRoutingMode === "target"
? null
: (upstreamUrl ?? null),
upstreamUrl: resolved.upstreamUrl,
apiKey: encryptedApiKey,
apiKeyLastChars,
authType: authType ?? null,
routingMode: resolvedRoutingMode,
authType: resolved.authType,
routingMode: resolved.routingMode,
skipTlsVerification: skipTlsVerification ?? false,
enabled: enabled ?? true,
createdAt: now,
+4 -15
View File
@@ -1,11 +1,6 @@
import type { AiModel, AiProvider } from "@server/db";
import type { PaginatedResponse } from "@server/types/Pagination";
import {
resolveAiProviderConfig,
type AiProviderAuthType,
type AiProviderRoutingMode,
type AiProviderType
} from "@server/lib/aiProviderDefaults";
import type { AiProviderAuthType } from "@server/lib/aiProviderDefaults";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
@@ -13,7 +8,7 @@ export type AiProviderPublic = Omit<AiProvider, "apiKey"> & {
/** Decrypted API key. Only included on get/create/update of a single provider. */
apiKey?: string | null;
effectiveUpstreamUrl: string | null;
effectiveAuthType: AiProviderAuthType | null;
effectiveAuthType: AiProviderAuthType;
};
export type ListAiProvidersResponse = PaginatedResponse<{
@@ -45,12 +40,6 @@ export function toPublicAiProvider(
options?: { includeApiKey?: boolean }
): AiProviderPublic {
const { apiKey: encryptedApiKey, ...rest } = provider;
const resolved = resolveAiProviderConfig({
type: provider.type as AiProviderType,
upstreamUrl: provider.upstreamUrl,
authType: provider.authType as AiProviderAuthType | null,
routingMode: provider.routingMode as AiProviderRoutingMode | null
});
let apiKey: string | null | undefined;
if (options?.includeApiKey) {
@@ -67,7 +56,7 @@ export function toPublicAiProvider(
return {
...rest,
...(options?.includeApiKey ? { apiKey } : {}),
effectiveUpstreamUrl: resolved.upstreamUrl,
effectiveAuthType: resolved.authType
effectiveUpstreamUrl: provider.upstreamUrl,
effectiveAuthType: provider.authType as AiProviderAuthType
};
}
+5 -12
View File
@@ -19,6 +19,7 @@ import {
refineProviderUpstreamFields
} from "@server/routers/aiProvider/validation";
import type {
AiProviderAuthType,
AiProviderRoutingMode,
AiProviderType
} from "@server/lib/aiProviderDefaults";
@@ -31,7 +32,7 @@ const bodySchema = z.strictObject({
name: z.string().nonempty().optional(),
upstreamUrl: z.url().optional().nullable(),
apiKey: z.string().optional(),
authType: aiAuthTypeSchema.optional().nullable(),
authType: aiAuthTypeSchema.optional(),
routingMode: aiRoutingModeSchema.optional(),
skipTlsVerification: z.boolean().optional(),
enabled: z.boolean().optional()
@@ -116,17 +117,16 @@ export async function updateAiProvider(
body.upstreamUrl !== undefined
? body.upstreamUrl
: existing.upstreamUrl;
const nextAuthType =
const nextAuthType: AiProviderAuthType =
body.authType !== undefined
? body.authType
: (existing.authType ??
(providerType === "custom" ? "bearer" : null));
: (existing.authType as AiProviderAuthType);
const validation = z
.object({
type: aiProviderTypeSchema,
upstreamUrl: z.string().nullable().optional(),
authType: aiAuthTypeSchema.nullable().optional(),
authType: aiAuthTypeSchema,
routingMode: aiRoutingModeSchema.optional()
})
.superRefine((data, ctx) => refineProviderUpstreamFields(data, ctx))
@@ -167,13 +167,6 @@ export async function updateAiProvider(
}
if (body.authType !== undefined) {
updateData.authType = body.authType;
} else if (
providerType === "custom" &&
!existing.authType &&
nextAuthType
) {
// Backfill required authType for custom providers created without one
updateData.authType = nextAuthType;
}
if (body.apiKey !== undefined) {
+4 -2
View File
@@ -1,6 +1,8 @@
import { z } from "zod";
import {
AI_PROVIDER_AUTH_TYPES,
providerRequiresUpstreamUrl,
type AiProviderAuthType,
type AiProviderRoutingMode,
type AiProviderType
} from "@server/lib/aiProviderDefaults";
@@ -17,7 +19,7 @@ export const aiProviderTypeSchema = z.enum([
"custom"
]);
export const aiAuthTypeSchema = z.enum(["bearer"]);
export const aiAuthTypeSchema = z.enum(AI_PROVIDER_AUTH_TYPES);
export const aiRoutingModeSchema = z.enum(["url", "target"]);
@@ -25,7 +27,7 @@ export function refineProviderUpstreamFields(
data: {
type: AiProviderType;
upstreamUrl?: string | null;
authType?: "bearer" | null;
authType?: AiProviderAuthType | null;
routingMode?: AiProviderRoutingMode | null;
},
ctx: z.RefinementCtx