add headers to provider

This commit is contained in:
miloschwartz
2026-08-05 17:42:58 -04:00
parent 1f3fff4a9d
commit 3d7e322bf9
13 changed files with 271 additions and 11 deletions
+6
View File
@@ -20,6 +20,7 @@ import { decrypt } from "@server/lib/crypto";
import {
AiProviderAuthType,
applyAiProviderAuthHeaders,
applyAiProviderCustomHeaders,
authTypeRequiresApiKey
} from "@server/lib/aiProviderDefaults";
import {
@@ -536,6 +537,11 @@ export async function handleAiGatewayProxy(
}
headers[key] = Array.isArray(value) ? value.join(", ") : value;
}
applyAiProviderCustomHeaders(
headers,
provider.headers,
config.getRawConfig().server.secret!
);
applyAiProviderAuthHeaders(headers, authType, apiKey);
// No dedicated per-request TLS agent is wired up (no extra deps for
@@ -15,10 +15,12 @@ import { toPublicAiProvider } from "@server/routers/aiProvider/types";
import {
aiAuthTypeSchema,
aiCapabilitiesSchema,
aiProviderHeadersSchema,
aiProviderTypeSchema,
aiRoutingModeSchema,
refineProviderUpstreamFields
} from "@server/routers/aiProvider/validation";
import { serializeAiProviderHeaders } from "@server/lib/aiProviderDefaults";
import {
resolveCapabilitiesForCreate,
serializeCapabilities
@@ -37,6 +39,7 @@ const bodySchema = z
authType: aiAuthTypeSchema.optional(),
routingMode: aiRoutingModeSchema.optional(),
capabilities: aiCapabilitiesSchema.optional(),
headers: aiProviderHeadersSchema,
skipTlsVerification: z.boolean().optional(),
enabled: z.boolean().optional()
})
@@ -101,6 +104,7 @@ export async function createAiProvider(
authType,
routingMode,
capabilities,
headers,
skipTlsVerification,
enabled
} = parsedBody.data;
@@ -132,6 +136,7 @@ export async function createAiProvider(
authType: resolved.authType,
routingMode: resolved.routingMode,
capabilities: serializeCapabilities(resolvedCapabilities),
headers: serializeAiProviderHeaders(headers, key),
skipTlsVerification: skipTlsVerification ?? false,
enabled: enabled ?? true,
createdAt: now,
+17 -3
View File
@@ -1,6 +1,10 @@
import type { AiModel, AiProvider } from "@server/db";
import type { PaginatedResponse } from "@server/types/Pagination";
import type { AiProviderAuthType } from "@server/lib/aiProviderDefaults";
import {
parseAiProviderHeaders,
type AiProviderAuthType,
type AiProviderHeader
} from "@server/lib/aiProviderDefaults";
import {
parseCapabilities,
type AiCapability
@@ -8,10 +12,13 @@ import {
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
export type AiProviderPublic = Omit<AiProvider, "apiKey" | "capabilities"> & {
/** Decrypted API key. Only included on get/create/update of a single provider. */
export type AiProviderPublic = Omit<
AiProvider,
"apiKey" | "capabilities" | "headers"
> & {
apiKey?: string | null;
capabilities: AiCapability[];
headers: AiProviderHeader[] | null;
effectiveUpstreamUrl: string | null;
effectiveAuthType: AiProviderAuthType;
};
@@ -47,6 +54,7 @@ export function toPublicAiProvider(
const {
apiKey: encryptedApiKey,
capabilities: rawCapabilities,
headers: rawHeaders,
...rest
} = provider;
@@ -62,10 +70,16 @@ export function toPublicAiProvider(
}
}
const parsedHeaders = parseAiProviderHeaders(
rawHeaders,
config.getRawConfig().server.secret!
);
return {
...rest,
...(options?.includeApiKey ? { apiKey } : {}),
capabilities: parseCapabilities(rawCapabilities),
headers: parsedHeaders.length > 0 ? parsedHeaders : null,
effectiveUpstreamUrl: provider.upstreamUrl,
effectiveAuthType: provider.authType as AiProviderAuthType
};
+12 -4
View File
@@ -15,14 +15,16 @@ import { toPublicAiProvider } from "@server/routers/aiProvider/types";
import {
aiAuthTypeSchema,
aiCapabilitiesSchema,
aiProviderHeadersSchema,
aiProviderTypeSchema,
aiRoutingModeSchema,
refineProviderUpstreamFields
} from "@server/routers/aiProvider/validation";
import type {
AiProviderAuthType,
AiProviderRoutingMode,
AiProviderType
import {
serializeAiProviderHeaders,
type AiProviderAuthType,
type AiProviderRoutingMode,
type AiProviderType
} from "@server/lib/aiProviderDefaults";
import {
parseCapabilities,
@@ -40,6 +42,7 @@ const bodySchema = z.strictObject({
authType: aiAuthTypeSchema.optional(),
routingMode: aiRoutingModeSchema.optional(),
capabilities: aiCapabilitiesSchema.optional(),
headers: aiProviderHeadersSchema,
skipTlsVerification: z.boolean().optional(),
enabled: z.boolean().optional()
});
@@ -202,6 +205,11 @@ export async function updateAiProvider(
updateData.apiKeyLastChars = body.apiKey.slice(-4);
}
if (body.headers !== undefined) {
const key = config.getRawConfig().server.secret!;
updateData.headers = serializeAiProviderHeaders(body.headers, key);
}
const [provider] = await db
.update(aiProviders)
.set(updateData)
+43
View File
@@ -28,6 +28,49 @@ export const aiCapabilitySchema = z.enum(AI_CAPABILITIES);
export const aiCapabilitiesSchema = z.array(aiCapabilitySchema);
const validHeaderName = /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/;
const validHeaderValue = /^[\t\x20-\x7E]*$/;
const templatePattern = /\{\{[^}]+\}\}/;
export const aiProviderHeadersSchema = z
.array(z.strictObject({ name: z.string(), value: z.string() }))
.nullable()
.optional()
.superRefine((headers, ctx) => {
if (!headers) {
return;
}
for (const [index, header] of headers.entries()) {
if (!validHeaderName.test(header.name)) {
ctx.addIssue({
code: "custom",
message:
"Header names may only contain valid HTTP token characters (letters, digits, and !#$%&'*+-.^_`|~).",
path: [index, "name"]
});
}
if (!validHeaderValue.test(header.value)) {
ctx.addIssue({
code: "custom",
message:
"Header values may only contain printable ASCII characters and horizontal whitespace.",
path: [index, "value"]
});
}
if (
templatePattern.test(header.name) ||
templatePattern.test(header.value)
) {
ctx.addIssue({
code: "custom",
message:
"Header names and values must not contain template expressions such as {{value}}.",
path: [index]
});
}
}
});
export function refineProviderUpstreamFields(
data: {
type: AiProviderType;