Spaces:
Runtime error
Runtime error
| import { z } from "zod"; | |
| import { | |
| ACCOUNT_FALLBACK_STRATEGY_VALUES, | |
| ROUTING_STRATEGY_VALUES, | |
| } from "@/shared/constants/routingStrategies"; | |
| import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints"; | |
| import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; | |
| import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; | |
| import { providerAllowsOptionalApiKey } from "@/shared/constants/providers"; | |
| import { | |
| OPENROUTER_PRESET_MAX_LENGTH, | |
| isOpenRouterPresetValue, | |
| } from "@/shared/constants/openRouterPreset"; | |
| import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; | |
| import { | |
| isForbiddenUpstreamHeaderName, | |
| isForbiddenCustomHeaderName, | |
| } from "@/shared/constants/upstreamHeaders"; | |
| import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts"; | |
| import { | |
| isHttpUrl, | |
| CODEX_REASONING_EFFORT_VALUES, | |
| REQUEST_DEFAULT_SERVICE_TIER_VALUES, | |
| upstreamHeadersRecordSchema, | |
| modelCompatPerProtocolSchema, | |
| customHeadersSchema, | |
| } from "./misc.ts"; | |
| export function validateProviderSpecificData( | |
| data: Record<string, unknown> | undefined, | |
| ctx: z.RefinementCtx | |
| ): void { | |
| if (!data) return; | |
| const baseUrl = data.baseUrl; | |
| if (baseUrl !== undefined && (typeof baseUrl !== "string" || !isHttpUrl(baseUrl))) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.baseUrl must be a valid http(s) URL", | |
| path: ["baseUrl"], | |
| }); | |
| } | |
| const customUserAgent = data.customUserAgent; | |
| if ( | |
| customUserAgent !== undefined && | |
| customUserAgent !== null && | |
| (typeof customUserAgent !== "string" || customUserAgent.length > 500) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.customUserAgent must be a string up to 500 chars", | |
| path: ["customUserAgent"], | |
| }); | |
| } | |
| const cx = data.cx; | |
| if (cx !== undefined && cx !== null && (typeof cx !== "string" || cx.length > 500)) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.cx must be a string up to 500 chars", | |
| path: ["cx"], | |
| }); | |
| } | |
| const region = data.region; | |
| if ( | |
| region !== undefined && | |
| region !== null && | |
| (typeof region !== "string" || region.length > 64) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.region must be a string up to 64 chars", | |
| path: ["region"], | |
| }); | |
| } | |
| const openaiStoreEnabled = data.openaiStoreEnabled; | |
| if (openaiStoreEnabled !== undefined && typeof openaiStoreEnabled !== "boolean") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.openaiStoreEnabled must be a boolean", | |
| path: ["openaiStoreEnabled"], | |
| }); | |
| } | |
| const blockExtraUsage = data.blockExtraUsage; | |
| if (blockExtraUsage !== undefined && typeof blockExtraUsage !== "boolean") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.blockExtraUsage must be a boolean", | |
| path: ["blockExtraUsage"], | |
| }); | |
| } | |
| const autoFetchModels = data.autoFetchModels; | |
| if (autoFetchModels !== undefined && typeof autoFetchModels !== "boolean") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.autoFetchModels must be a boolean", | |
| path: ["autoFetchModels"], | |
| }); | |
| } | |
| const disableStreamOptions = data.disableStreamOptions; | |
| if (disableStreamOptions !== undefined && typeof disableStreamOptions !== "boolean") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.disableStreamOptions must be a boolean", | |
| path: ["disableStreamOptions"], | |
| }); | |
| } | |
| const preset = data.preset; | |
| if (preset !== undefined && preset !== null && !isOpenRouterPresetValue(preset)) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: `providerSpecificData.preset must be a string up to ${OPENROUTER_PRESET_MAX_LENGTH} chars`, | |
| path: ["preset"], | |
| }); | |
| } | |
| const requestDefaults = data.requestDefaults; | |
| if (requestDefaults !== undefined) { | |
| if (!requestDefaults || typeof requestDefaults !== "object" || Array.isArray(requestDefaults)) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.requestDefaults must be an object", | |
| path: ["requestDefaults"], | |
| }); | |
| } else { | |
| const requestDefaultsRecord = requestDefaults as Record<string, unknown>; | |
| const reasoningEffort = requestDefaultsRecord.reasoningEffort; | |
| if ( | |
| reasoningEffort !== undefined && | |
| reasoningEffort !== null && | |
| (typeof reasoningEffort !== "string" || | |
| !CODEX_REASONING_EFFORT_VALUES.has(reasoningEffort.trim().toLowerCase())) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: | |
| "providerSpecificData.requestDefaults.reasoningEffort must be one of none, low, medium, high, xhigh", | |
| path: ["requestDefaults", "reasoningEffort"], | |
| }); | |
| } | |
| const serviceTier = requestDefaultsRecord.serviceTier; | |
| if ( | |
| serviceTier !== undefined && | |
| serviceTier !== null && | |
| (typeof serviceTier !== "string" || | |
| !REQUEST_DEFAULT_SERVICE_TIER_VALUES.has(serviceTier.trim().toLowerCase())) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: | |
| "providerSpecificData.requestDefaults.serviceTier must be one of default, priority, fast, flex when provided", | |
| path: ["requestDefaults", "serviceTier"], | |
| }); | |
| } | |
| const context1m = requestDefaultsRecord.context1m; | |
| if (context1m !== undefined && context1m !== null && typeof context1m !== "boolean") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.requestDefaults.context1m must be a boolean", | |
| path: ["requestDefaults", "context1m"], | |
| }); | |
| } | |
| for (const booleanKey of ["redactThinking", "summarizeThinking"] as const) { | |
| const value = requestDefaultsRecord[booleanKey]; | |
| if (value === undefined || value === null || typeof value === "boolean") continue; | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: `providerSpecificData.requestDefaults.${booleanKey} must be a boolean`, | |
| path: ["requestDefaults", booleanKey], | |
| }); | |
| } | |
| } | |
| } | |
| // [Oracle CONDITIONAL] consoleApiKeyλ bailian-coding-plan μ μ© νλ. | |
| // λ€λ₯Έ νλ‘λ°μ΄λ κ³΅ν΅ κ·μ½μΌλ‘ μ¬μ¬μ©νμ§ μλλ€. | |
| const consoleApiKey = data.consoleApiKey; | |
| if (consoleApiKey !== undefined && consoleApiKey !== null && typeof consoleApiKey !== "string") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.consoleApiKey must be a string", | |
| path: ["consoleApiKey"], | |
| }); | |
| } | |
| if (typeof consoleApiKey === "string" && consoleApiKey.length > 10000) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.consoleApiKey must be at most 10000 characters", | |
| path: ["consoleApiKey"], | |
| }); | |
| } | |
| for (const key of ["openCodeGoWorkspaceId", "opencodeGoWorkspaceId", "workspaceId"] as const) { | |
| const value = data[key]; | |
| if (value !== undefined && value !== null && typeof value !== "string") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: `providerSpecificData.${key} must be a string`, | |
| path: [key], | |
| }); | |
| } | |
| if (typeof value === "string" && value.length > 1000) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: `providerSpecificData.${key} must be at most 1000 characters`, | |
| path: [key], | |
| }); | |
| } | |
| } | |
| for (const key of [ | |
| "openCodeGoAuthCookie", | |
| "opencodeGoAuthCookie", | |
| "authCookie", | |
| "ollamaUsageCookie", | |
| "ollamaCloudUsageCookie", | |
| "ollamaCloudCookie", | |
| "usageCookie", | |
| ] as const) { | |
| const value = data[key]; | |
| if (value !== undefined && value !== null && typeof value !== "string") { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: `providerSpecificData.${key} must be a string`, | |
| path: [key], | |
| }); | |
| } | |
| if (typeof value === "string" && value.length > 10000) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: `providerSpecificData.${key} must be at most 10000 characters`, | |
| path: [key], | |
| }); | |
| } | |
| } | |
| const groupTag = data.tag; | |
| if ( | |
| groupTag !== undefined && | |
| groupTag !== null && | |
| (typeof groupTag !== "string" || groupTag.length > 100) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.tag must be a string up to 100 chars", | |
| path: ["tag"], | |
| }); | |
| } | |
| const routingTags = data.tags; | |
| if (routingTags !== undefined && routingTags !== null) { | |
| if (!Array.isArray(routingTags) || routingTags.length > 50) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.tags must be an array with at most 50 items", | |
| path: ["tags"], | |
| }); | |
| } else if ( | |
| routingTags.some( | |
| (tag) => typeof tag !== "string" || tag.trim().length === 0 || tag.trim().length > 64 | |
| ) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: | |
| "providerSpecificData.tags must contain non-empty strings up to 64 characters each", | |
| path: ["tags"], | |
| }); | |
| } | |
| } | |
| const excludedModels = data.excludedModels ?? data.excluded_models; | |
| if (excludedModels !== undefined && excludedModels !== null) { | |
| if (typeof excludedModels === "string") { | |
| if (excludedModels.length > 5000) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.excludedModels string must be up to 5000 chars", | |
| path: ["excludedModels"], | |
| }); | |
| } | |
| } else if (!Array.isArray(excludedModels) || excludedModels.length > 100) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerSpecificData.excludedModels must be an array with at most 100 items", | |
| path: ["excludedModels"], | |
| }); | |
| } else if ( | |
| excludedModels.some( | |
| (pattern) => | |
| typeof pattern !== "string" || | |
| pattern.trim().length === 0 || | |
| pattern.trim().length > 200 || | |
| pattern.trim() === "**" | |
| ) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: | |
| "providerSpecificData.excludedModels must contain non-empty patterns up to 200 characters", | |
| path: ["excludedModels"], | |
| }); | |
| } | |
| } | |
| const clientProfile = data.clientProfile; | |
| if (clientProfile !== undefined && clientProfile !== null) { | |
| const normalized = typeof clientProfile === "string" ? clientProfile.trim().toLowerCase() : ""; | |
| if ( | |
| typeof clientProfile !== "string" || | |
| !["ide", "harness", "cli", "sdk"].includes(normalized) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: | |
| "providerSpecificData.clientProfile must be ide, harness, cli, or sdk (cli/sdk map to harness)", | |
| path: ["clientProfile"], | |
| }); | |
| } | |
| } | |
| } | |
| // ββββ Provider Schemas ββββ | |
| export const createProviderSchema = z | |
| .object({ | |
| provider: z.string().min(1).max(100), | |
| apiKey: z.string().max(10000).optional(), | |
| name: z.string().min(1).max(200), | |
| priority: z.number().int().min(1).max(100).optional(), | |
| globalPriority: z.number().int().min(1).max(100).nullable().optional(), | |
| defaultModel: z.string().max(200).nullable().optional(), | |
| testStatus: z.string().max(50).optional(), | |
| providerSpecificData: z | |
| .record(z.string(), z.unknown()) | |
| .optional() | |
| .superRefine((data, ctx) => { | |
| validateProviderSpecificData(data, ctx); | |
| }), | |
| }) | |
| .superRefine((data, ctx) => { | |
| const apiKey = typeof data.apiKey === "string" ? data.apiKey.trim() : ""; | |
| const apiKeyOptional = providerAllowsOptionalApiKey(data.provider); | |
| if (!apiKeyOptional && apiKey.length === 0) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "API key is required", | |
| path: ["apiKey"], | |
| }); | |
| } | |
| const cx = | |
| data.providerSpecificData && typeof data.providerSpecificData === "object" | |
| ? (data.providerSpecificData as Record<string, unknown>).cx | |
| : undefined; | |
| if ( | |
| data.provider === "google-pse-search" && | |
| (typeof cx !== "string" || cx.trim().length === 0) | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "Programmable Search Engine ID (cx) is required", | |
| path: ["providerSpecificData", "cx"], | |
| }); | |
| } | |
| }); | |
| export const bulkCreateProviderSchema = z | |
| .object({ | |
| provider: z.string().min(1).max(100), | |
| entries: z | |
| .array( | |
| z.object({ | |
| name: z.string().min(1).max(200), | |
| apiKey: z.string().min(1).max(10000), | |
| }) | |
| ) | |
| .min(1, "entries must contain at least 1 item") | |
| .max(200, "entries must contain at most 200 items"), | |
| priority: z.number().int().min(1).max(100).optional(), | |
| globalPriority: z.number().int().min(1).max(100).nullable().optional(), | |
| providerSpecificData: z | |
| .record(z.string(), z.unknown()) | |
| .optional() | |
| .superRefine((data, ctx) => { | |
| validateProviderSpecificData(data, ctx); | |
| }), | |
| validateKeys: z.boolean().optional(), | |
| }) | |
| .superRefine((data, ctx) => { | |
| if (data.provider === "google-pse-search") { | |
| const cx = | |
| data.providerSpecificData && typeof data.providerSpecificData === "object" | |
| ? (data.providerSpecificData as Record<string, unknown>).cx | |
| : undefined; | |
| if (typeof cx !== "string" || cx.trim().length === 0) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "Programmable Search Engine ID (cx) is required", | |
| path: ["providerSpecificData", "cx"], | |
| }); | |
| } | |
| } | |
| }); | |
| // ββββ Bulk Web-Session Import Schema ββββ | |
| export const bulkWebSessionImportSchema = z.object({ | |
| provider: z.string().min(1).max(100), | |
| entries: z | |
| .array( | |
| z.object({ | |
| name: z.string().min(1).max(200), | |
| credential: z | |
| .string() | |
| .min(1) | |
| .max(64 * 1024, "Credential must be under 64 KB"), | |
| }) | |
| ) | |
| .min(1, "entries must contain at least 1 item") | |
| .max(50, "entries must contain at most 50 items"), | |
| priority: z.number().int().min(1).max(100).optional(), | |
| globalPriority: z.number().int().min(1).max(100).nullable().optional(), | |
| }); | |
| export const providerModelMutationSchema = z.object({ | |
| provider: z.string().trim().min(1, "provider is required").max(120), | |
| modelId: z.string().trim().min(1, "modelId is required").max(240), | |
| modelName: z.string().trim().max(240).optional(), | |
| source: z.string().trim().max(80).optional(), | |
| apiFormat: z | |
| .enum([ | |
| "chat-completions", | |
| "responses", | |
| "embeddings", | |
| "rerank", | |
| "audio-transcriptions", | |
| "audio-speech", | |
| "images-generations", | |
| ]) | |
| .default("chat-completions"), | |
| supportedEndpoints: z | |
| .array( | |
| z.enum([ | |
| "chat", | |
| "embeddings", | |
| "rerank", | |
| "images", | |
| "audio", | |
| "audio-transcriptions", | |
| "audio-speech", | |
| "images-generations", | |
| ]) | |
| ) | |
| .default(["chat"]), | |
| // #2905: optional per-model wire format override for custom models (e.g. a | |
| // custom opencode-go model that must use the Anthropic Messages shape). | |
| targetFormat: z | |
| .enum(["openai", "openai-responses", "claude", "gemini", "gemini-cli", "antigravity"]) | |
| .optional(), | |
| // #1294: optional token limits set in the "add custom model" form. The wire | |
| // shape uses max_input_tokens / max_output_tokens (mirrors the /v1/models | |
| // catalog); they persist as inputTokenLimit / outputTokenLimit. | |
| max_input_tokens: z.number().int().positive().optional(), | |
| max_output_tokens: z.number().int().positive().optional(), | |
| normalizeToolCallId: z.boolean().optional(), | |
| preserveOpenAIDeveloperRole: z.boolean().nullable().optional(), | |
| upstreamHeaders: upstreamHeadersRecordSchema.nullable().optional(), | |
| /** Zod 4: `z.record(z.enum([...]), β¦)` requires every enum key; use `partialRecord` for sparse patches. */ | |
| compatByProtocol: z | |
| .partialRecord(z.enum(["openai", "openai-responses", "claude"]), modelCompatPerProtocolSchema) | |
| .optional(), | |
| }); | |
| export const updateModelAliasesSchema = z.object({ | |
| aliases: z.record(z.string().trim().min(1), z.string().trim().min(1)), | |
| }); | |
| export const addModelAliasSchema = z.object({ | |
| from: z.string().trim().min(1), | |
| to: z.string().trim().min(1), | |
| }); | |
| export const removeModelAliasSchema = z.object({ | |
| from: z.string().trim().min(1), | |
| }); | |
| export const createProviderNodeSchema = z | |
| .object({ | |
| name: z.string().trim().min(1, "Name is required"), | |
| prefix: z.string().trim().min(1, "Prefix is required"), | |
| apiType: z | |
| .enum([ | |
| "chat", | |
| "responses", | |
| "embeddings", | |
| "audio-transcriptions", | |
| "audio-speech", | |
| "images-generations", | |
| ]) | |
| .optional(), | |
| baseUrl: z.string().trim().min(1).optional(), | |
| type: z.enum(["openai-compatible", "anthropic-compatible"]).optional(), | |
| compatMode: z.enum(["cc"]).optional(), | |
| chatPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")), | |
| modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")), | |
| customHeaders: customHeadersSchema, | |
| }) | |
| .superRefine((value, ctx) => { | |
| const nodeType = value.type || "openai-compatible"; | |
| if (nodeType === "openai-compatible" && !value.apiType) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "Invalid OpenAI compatible API type", | |
| path: ["apiType"], | |
| }); | |
| } | |
| }); | |
| export const updateProviderNodeSchema = z.object({ | |
| name: z.string().trim().min(1, "Name is required"), | |
| prefix: z.string().trim().min(1, "Prefix is required"), | |
| apiType: z | |
| .enum([ | |
| "chat", | |
| "responses", | |
| "embeddings", | |
| "audio-transcriptions", | |
| "audio-speech", | |
| "images-generations", | |
| ]) | |
| .optional(), | |
| baseUrl: z.string().trim().min(1, "Base URL is required"), | |
| chatPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")), | |
| modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")), | |
| customHeaders: customHeadersSchema, | |
| }); | |
| export const providerNodeValidateSchema = z.object({ | |
| baseUrl: z.string().trim().min(1, "Base URL and API key required"), | |
| apiKey: z.string().trim().optional(), | |
| type: z.enum(["openai-compatible", "anthropic-compatible"]).optional(), | |
| compatMode: z.enum(["cc"]).optional(), | |
| chatPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")), | |
| modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")), | |
| modelId: z.string().trim().max(200).optional().or(z.literal("")), | |
| }); | |
| export const updateProviderConnectionSchema = z | |
| .object({ | |
| name: z.string().max(200).optional(), | |
| priority: z.coerce.number().int().min(1).max(100).optional(), | |
| globalPriority: z.union([z.coerce.number().int().min(1).max(100), z.null()]).optional(), | |
| defaultModel: z.union([z.string().max(200), z.null()]).optional(), | |
| isActive: z.boolean().optional(), | |
| apiKey: z.string().max(10000).optional(), | |
| testStatus: z.string().max(50).optional(), | |
| lastError: z.union([z.string(), z.null()]).optional(), | |
| lastErrorAt: z.union([z.string(), z.null()]).optional(), | |
| lastErrorType: z.union([z.string(), z.null()]).optional(), | |
| lastErrorSource: z.union([z.string(), z.null()]).optional(), | |
| errorCode: z.union([z.string(), z.null()]).optional(), | |
| rateLimitedUntil: z.union([z.string(), z.null()]).optional(), | |
| lastTested: z.union([z.string(), z.null()]).optional(), | |
| healthCheckInterval: z.coerce.number().int().min(0).optional(), | |
| group: z.union([z.string().max(100), z.null()]).optional(), | |
| maxConcurrent: z.union([z.null(), z.coerce.number().int().min(0)]).optional(), | |
| // Per-window quota cutoffs. Map keys are window names (e.g. "window5h", | |
| // "window7d"); values are 0-100 integers, or null to clear that window's | |
| // override (the API route merges this into the existing map and prunes | |
| // null entries before persisting). The whole field set to null clears | |
| // every override on the connection. | |
| quotaWindowThresholds: z | |
| .union([ | |
| z.null(), | |
| z.record( | |
| // Window keys mirror the quota names from getUsageForProvider β | |
| // bound for defense-in-depth so a malicious payload can't ship | |
| // megabyte-long keys that would bloat the DB row. | |
| z.string().min(1).max(64), | |
| z.union([z.null(), z.coerce.number().int().min(0).max(100)]) | |
| ), | |
| ]) | |
| .optional(), | |
| projectId: z.union([z.string(), z.null()]).optional(), | |
| // Per-connection rate limit overrides β overrides the global RequestQueueSettings | |
| // for this connection. Set to null to clear all overrides. | |
| rateLimitOverrides: z | |
| .union([ | |
| z.null(), | |
| z.object({ | |
| rpm: z.coerce.number().int().min(0).max(1_000_000).optional(), | |
| tpm: z.coerce.number().int().min(0).max(100_000_000).optional(), | |
| tpd: z.coerce.number().int().min(0).max(10_000_000_000).optional(), | |
| minTime: z.coerce.number().int().min(0).max(60_000).optional(), | |
| maxConcurrent: z.coerce.number().int().min(0).max(10_000).optional(), | |
| }), | |
| ]) | |
| .optional(), | |
| proxyEnabled: z.boolean().optional(), | |
| perKeyProxyEnabled: z.boolean().optional(), | |
| // Partial patch of per-connection provider-specific settings (e.g. quota toggles) | |
| providerSpecificData: z | |
| .record(z.string(), z.unknown()) | |
| .optional() | |
| .superRefine((data, ctx) => { | |
| validateProviderSpecificData(data, ctx); | |
| }), | |
| }) | |
| .superRefine((value, ctx) => { | |
| if (Object.keys(value).length === 0) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "No valid fields to update", | |
| path: [], | |
| }); | |
| } | |
| }); | |
| export const providersBatchTestSchema = z | |
| .object({ | |
| mode: z.enum([ | |
| "provider", | |
| "oauth", | |
| "free", | |
| "no-auth", | |
| "apikey", | |
| "compatible", | |
| "all", | |
| "web-cookie", | |
| "search", | |
| "audio", | |
| "local", | |
| "upstream-proxy", | |
| "cloud-agent", | |
| "ide", | |
| "selected", | |
| ]), | |
| // Frontend may send null when mode != 'provider' β accept and treat as missing | |
| providerId: z.string().trim().min(1).nullable().optional(), | |
| // Explicit connection IDs to test β required when mode=selected | |
| connectionIds: z.array(z.string().trim().min(1)).max(100).nullable().optional(), | |
| }) | |
| .superRefine((value, ctx) => { | |
| // Treat null same as undefined | |
| const pid = value.providerId ?? null; | |
| if (value.mode === "provider" && !pid) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "providerId is required when mode=provider", | |
| path: ["providerId"], | |
| }); | |
| } | |
| const ids = value.connectionIds ?? null; | |
| if (value.mode === "selected" && (!ids || ids.length === 0)) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "connectionIds is required when mode=selected", | |
| path: ["connectionIds"], | |
| }); | |
| } | |
| }); | |
| // PATCH /api/providers β bulk activate/deactivate selected connections | |
| export const batchUpdateProviderConnectionsSchema = z.object({ | |
| ids: z.array(z.string().trim().min(1)).min(1).max(100), | |
| isActive: z.boolean(), | |
| }); | |
| export const validateProviderApiKeySchema = z | |
| .object({ | |
| provider: z.string().trim().min(1, "Provider and API key required"), | |
| apiKey: z.string().trim().optional(), | |
| validationModelId: z.string().trim().optional(), | |
| customUserAgent: z.string().trim().max(500).optional(), | |
| baseUrl: z.string().trim().url().optional(), | |
| region: z.string().trim().max(64).optional(), | |
| cx: z.string().trim().max(500).optional(), | |
| }) | |
| .superRefine((data, ctx) => { | |
| if (data.provider === "google-pse-search" && !data.cx) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "Programmable Search Engine ID (cx) is required", | |
| path: ["cx"], | |
| }); | |
| } | |
| }); | |