first commit
This commit is contained in:
260
ui/lib/schemas/providerForm.ts
Normal file
260
ui/lib/schemas/providerForm.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { KnownProvidersNames } from "@/lib/constants/logs";
|
||||
import { isValidAliases, isValidVertexAuthCredentials } from "@/lib/utils/validation";
|
||||
import { z } from "zod";
|
||||
|
||||
// Base schemas for reusable types
|
||||
const ProxyTypeSchema = z.enum(["none", "http", "socks5", "environment"]);
|
||||
|
||||
const ProxyConfigSchema = z
|
||||
.object({
|
||||
type: ProxyTypeSchema,
|
||||
url: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
})
|
||||
.superRefine((v, ctx) => {
|
||||
const needsUrl = v.type === "http" || v.type === "socks5";
|
||||
if (needsUrl && !(v.url && v.url.trim())) {
|
||||
ctx.addIssue({ code: "custom", path: ["url"], message: "Proxy URL is required for http/socks5" });
|
||||
}
|
||||
const user = v.username?.trim();
|
||||
const pass = v.password?.trim();
|
||||
if ((user && !pass) || (pass && !user)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["password"],
|
||||
message: "Username and password must both be provided",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const NetworkConfigSchema = z
|
||||
.object({
|
||||
base_url: z.string().optional(),
|
||||
extra_headers: z.record(z.string(), z.string()).optional(),
|
||||
default_request_timeout_in_seconds: z.number().min(1, "Timeout must be greater than 0 seconds"),
|
||||
max_retries: z.number().min(0, "Max retries cannot be negative"),
|
||||
retry_backoff_initial: z.number(),
|
||||
retry_backoff_max: z.number(),
|
||||
})
|
||||
.refine((v) => v.retry_backoff_initial <= v.retry_backoff_max, {
|
||||
message: "Initial backoff must be <= max backoff",
|
||||
path: ["retry_backoff_initial"],
|
||||
});
|
||||
|
||||
const ConcurrencyAndBufferSizeSchema = z
|
||||
.object({
|
||||
concurrency: z.number().min(1, "Concurrency must be greater than 0"),
|
||||
buffer_size: z.number().min(1, "Buffer size must be greater than 0"),
|
||||
})
|
||||
.refine((data) => data.concurrency <= data.buffer_size, {
|
||||
message: "Concurrency must be less than or equal to buffer size",
|
||||
path: ["concurrency"],
|
||||
});
|
||||
|
||||
const AllowedRequestsSchema = z.object({
|
||||
text_completion: z.boolean(),
|
||||
chat_completion: z.boolean(),
|
||||
chat_completion_stream: z.boolean(),
|
||||
responses: z.boolean(),
|
||||
responses_stream: z.boolean(),
|
||||
embedding: z.boolean(),
|
||||
speech: z.boolean(),
|
||||
speech_stream: z.boolean(),
|
||||
transcription: z.boolean(),
|
||||
transcription_stream: z.boolean(),
|
||||
});
|
||||
|
||||
// Key configuration schemas
|
||||
const AzureKeyConfigSchema = z.object({
|
||||
endpoint: z.string().min(1, "Endpoint is required for Azure keys"),
|
||||
api_version: z.string().optional(),
|
||||
client_id: z.string().optional(),
|
||||
client_secret: z.string().optional(),
|
||||
tenant_id: z.string().optional(),
|
||||
});
|
||||
|
||||
const VertexKeyConfigSchema = z.object({
|
||||
project_id: z.string().min(1, "Project ID is required for Vertex AI keys"),
|
||||
project_number: z.string().optional(),
|
||||
region: z.string().min(1, "Region is required for Vertex AI keys"),
|
||||
auth_credentials: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => !value || isValidVertexAuthCredentials(value), {
|
||||
message: "Auth Credentials must be a valid JSON object or env.VAR format when provided",
|
||||
}),
|
||||
});
|
||||
|
||||
// S3 bucket configuration for Bedrock batch operations
|
||||
const S3BucketConfigSchema = z.object({
|
||||
bucket_name: z.string().min(1, "Bucket name is required"),
|
||||
prefix: z.string().optional(),
|
||||
is_default: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const BatchS3ConfigSchema = z.object({
|
||||
buckets: z.array(S3BucketConfigSchema).optional(),
|
||||
});
|
||||
|
||||
const BedrockKeyConfigSchema = z
|
||||
.object({
|
||||
access_key: z.string(),
|
||||
secret_key: z.string(),
|
||||
session_token: z.string().optional(),
|
||||
region: z.string().min(1, "Region is required for Bedrock keys"),
|
||||
role_arn: z.string().optional(),
|
||||
external_id: z.string().optional(),
|
||||
session_name: z.string().optional(),
|
||||
arn: z.string().optional(),
|
||||
batch_s3_config: BatchS3ConfigSchema.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const accessKey = data.access_key?.trim() || "";
|
||||
const secretKey = data.secret_key?.trim() || "";
|
||||
const bothEmpty = accessKey === "" && secretKey === "";
|
||||
const bothProvided = accessKey !== "" && secretKey !== "";
|
||||
|
||||
// Either both empty (IAM role auth) or both provided (explicit credentials)
|
||||
if (!bothEmpty && !bothProvided) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for session token when using IAM role path (both keys empty)
|
||||
const sessionToken = data.session_token?.trim() || "";
|
||||
if (bothEmpty && sessionToken !== "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "For Bedrock: either provide both Access Key and Secret Key, or leave both empty for IAM role authentication",
|
||||
path: ["access_key"],
|
||||
},
|
||||
);
|
||||
|
||||
const ReplicateKeyConfigSchema = z.object({
|
||||
use_deployments_endpoint: z.boolean(),
|
||||
});
|
||||
|
||||
const KeySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1, "Name is required for the key"),
|
||||
value: z.string(),
|
||||
models: z.array(z.string()),
|
||||
weight: z.number().min(0.1, "Key weights must be between 0.1 and 1").max(1, "Key weights must be between 0.1 and 1"),
|
||||
aliases: z
|
||||
.union([z.record(z.string(), z.string()), z.string()])
|
||||
.optional()
|
||||
.refine((value) => !value || isValidAliases(value), { message: "Aliases must be a valid JSON object" }),
|
||||
azure_key_config: AzureKeyConfigSchema.optional(),
|
||||
vertex_key_config: VertexKeyConfigSchema.optional(),
|
||||
bedrock_key_config: BedrockKeyConfigSchema.optional(),
|
||||
replicate_key_config: ReplicateKeyConfigSchema.optional(),
|
||||
use_for_batch_api: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// Main provider form schema
|
||||
export const ProviderFormSchema = z
|
||||
.object({
|
||||
selectedProvider: z.string().min(1, "Please select a provider"),
|
||||
customProviderName: z.string().optional(),
|
||||
baseProviderType: z.enum([...KnownProvidersNames, ""]).optional(),
|
||||
keys: z.array(KeySchema),
|
||||
networkConfig: NetworkConfigSchema.optional(),
|
||||
performanceConfig: ConcurrencyAndBufferSizeSchema.optional(),
|
||||
proxyConfig: ProxyConfigSchema.optional(),
|
||||
sendBackRawResponse: z.boolean().default(false),
|
||||
allowedRequests: AllowedRequestsSchema.optional(),
|
||||
isDirty: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
// Custom provider validation
|
||||
const isCustomProvider =
|
||||
data.selectedProvider === "custom" ||
|
||||
!!data.customProviderName ||
|
||||
!!data.baseProviderType ||
|
||||
!KnownProvidersNames.includes(data.selectedProvider as (typeof KnownProvidersNames)[number]);
|
||||
|
||||
if (isCustomProvider) {
|
||||
if (!data.customProviderName?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Custom provider name is required",
|
||||
path: ["customProviderName"],
|
||||
});
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9_-]+$/.test(data.customProviderName?.trim() || "")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Custom provider name must be lowercase alphanumeric and may include '-' or '_' (no spaces)",
|
||||
path: ["customProviderName"],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.baseProviderType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Base provider type is required for custom providers",
|
||||
path: ["baseProviderType"],
|
||||
});
|
||||
}
|
||||
|
||||
if (KnownProvidersNames.includes(data.customProviderName?.trim() as (typeof KnownProvidersNames)[number])) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Custom provider name cannot be the same as a standard provider name",
|
||||
path: ["customProviderName"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Base URL validation for specific providers
|
||||
const baseURLRequired = isCustomProvider;
|
||||
if (baseURLRequired) {
|
||||
if (!data.networkConfig?.base_url) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Base URL is required for this provider",
|
||||
path: ["networkConfig", "base_url"],
|
||||
});
|
||||
}
|
||||
|
||||
if (data.networkConfig?.base_url && !/^https?:\/\/.+/.test(data.networkConfig.base_url)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Base URL must start with http:// or https://",
|
||||
path: ["networkConfig", "base_url"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keys validation
|
||||
const keysRequired = data.selectedProvider === "custom" || !["ollama", "sgl"].includes(data.selectedProvider);
|
||||
if (keysRequired) {
|
||||
if (data.keys.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one API key is required",
|
||||
path: ["keys"],
|
||||
});
|
||||
}
|
||||
|
||||
// Validate individual key values based on provider type
|
||||
const effectiveProviderType = data.baseProviderType || data.selectedProvider;
|
||||
data.keys.forEach((key, index) => {
|
||||
if (effectiveProviderType !== "vertex" && effectiveProviderType !== "bedrock" && !key.value.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "API key value cannot be empty",
|
||||
path: ["keys", index, "value"],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type ProviderFormData = z.infer<typeof ProviderFormSchema>;
|
||||
Reference in New Issue
Block a user