import { z } from "zod";
import { stringToHttpHeadersSchema } from "@phoenix/schemas/httpHeadersSchema";
/**
* HTTP headers field schema with RFC 7230 validation.
* Validates:
* - Valid JSON syntax
* - Must be an object (not array)
* - Header names follow RFC 7230 (no spaces, only allowed characters)
* - Header values contain only visible ASCII characters
* - No duplicate keys (case-insensitive, since HTTP headers are case-insensitive)
*/
const httpHeadersFieldSchema = z
.string()
.optional()
.superRefine((val, ctx) => {
// Empty/undefined values are valid (field is optional)
if (!val || val.trim() === "" || val.trim() === "{}") {
return;
}
const result = stringToHttpHeadersSchema.safeParse(val);
if (!result.success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
result.error.errors[0]?.message || "Invalid HTTP headers format",
});
}
});
// URL field schema - validates URLs but allows empty strings (optional fields)
const urlFieldSchema = z
.string()
.optional()
.refine(
(val) => {
if (!val || val.trim() === "") return true;
try {
new URL(val);
return true;
} catch {
return false;
}
},
{ message: "Must be a valid URL" }
);
// Base schema shared by all providers
const baseProviderSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
provider: z.string().min(1, "Provider is required"),
});
// OpenAI schema
const openAISchema = baseProviderSchema.extend({
sdk: z.literal("OPENAI"),
openai_api_type: z.enum(["CHAT_COMPLETIONS", "RESPONSES"]),
openai_api_key: z.string().min(1, "API key is required"),
openai_base_url: urlFieldSchema,
openai_organization: z.string().optional(),
openai_project: z.string().optional(),
openai_default_headers: httpHeadersFieldSchema,
});
// Azure OpenAI schema - endpoint requires URL validation with non-empty check
const azureEndpointSchema = z
.string()
.min(1, "Endpoint is required")
.refine(
(val) => {
try {
new URL(val);
return true;
} catch {
return false;
}
},
{ message: "Must be a valid URL" }
);
const azureOpenAISchema = baseProviderSchema.extend({
sdk: z.literal("AZURE_OPENAI"),
openai_api_type: z.enum(["CHAT_COMPLETIONS", "RESPONSES"]),
azure_endpoint: azureEndpointSchema,
azure_auth_method: z.enum([
"api_key",
"ad_token_provider",
"default_credentials",
]),
azure_api_key: z.string().optional(),
azure_tenant_id: z.string().optional(),
azure_client_id: z.string().optional(),
azure_client_secret: z.string().optional(),
azure_scope: z.string().optional(),
azure_default_headers: httpHeadersFieldSchema,
});
// Anthropic schema
const anthropicSchema = baseProviderSchema.extend({
sdk: z.literal("ANTHROPIC"),
anthropic_api_key: z.string().min(1, "API key is required"),
anthropic_base_url: urlFieldSchema,
anthropic_default_headers: httpHeadersFieldSchema,
});
// AWS Bedrock schema
const awsBedrockSchema = baseProviderSchema.extend({
sdk: z.literal("AWS_BEDROCK"),
aws_region: z.string().min(1, "Region is required"),
aws_auth_method: z.enum(["access_keys", "default_credentials"]),
aws_access_key_id: z.string().optional(),
aws_secret_access_key: z.string().optional(),
aws_session_token: z.string().optional(),
aws_endpoint_url: urlFieldSchema,
});
// Google GenAI schema
const googleGenAISchema = baseProviderSchema.extend({
sdk: z.literal("GOOGLE_GENAI"),
google_api_key: z.string().min(1, "API key is required"),
google_base_url: urlFieldSchema,
google_headers: httpHeadersFieldSchema,
});
// Discriminated union of all provider schemas
const baseProviderFormSchema = z.discriminatedUnion("sdk", [
openAISchema,
azureOpenAISchema,
anthropicSchema,
awsBedrockSchema,
googleGenAISchema,
]);
// Add conditional validation for auth methods on top of the discriminated union
export const providerFormSchema = baseProviderFormSchema.superRefine(
(data, ctx) => {
// Azure OpenAI conditional validation
if (data.sdk === "AZURE_OPENAI") {
if (data.azure_auth_method === "api_key") {
// API key is required when using api_key auth
if (!data.azure_api_key || data.azure_api_key.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "API key is required when using API key authentication",
path: ["azure_api_key"],
});
}
} else if (data.azure_auth_method === "ad_token_provider") {
// AD fields are required when using AD token provider
if (!data.azure_tenant_id || data.azure_tenant_id.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Tenant ID is required for AD Token Provider authentication",
path: ["azure_tenant_id"],
});
}
if (!data.azure_client_id || data.azure_client_id.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Client ID is required for AD Token Provider authentication",
path: ["azure_client_id"],
});
}
if (
!data.azure_client_secret ||
data.azure_client_secret.length === 0
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Client Secret is required for AD Token Provider authentication",
path: ["azure_client_secret"],
});
}
}
// No validation needed for "default_credentials" - it has no required fields
}
// AWS Bedrock conditional validation
if (data.sdk === "AWS_BEDROCK") {
if (data.aws_auth_method === "access_keys") {
// Access keys are required when using access_keys auth
if (!data.aws_access_key_id || data.aws_access_key_id.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Access Key ID is required when using Access Keys authentication",
path: ["aws_access_key_id"],
});
}
if (
!data.aws_secret_access_key ||
data.aws_secret_access_key.length === 0
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Secret Access Key is required when using Access Keys authentication",
path: ["aws_secret_access_key"],
});
}
}
// No validation needed for "default_credentials" - it has no required fields
}
}
);