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 { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; | |
| import { HIDEABLE_SIDEBAR_GROUP_IDS } from "@/shared/constants/sidebarGroupVisibility"; | |
| import { | |
| isForbiddenUpstreamHeaderName, | |
| isForbiddenCustomHeaderName, | |
| } from "@/shared/constants/upstreamHeaders"; | |
| import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts"; | |
| // ββββ Settings Schemas ββββ | |
| // FASE-01: Removed .passthrough() β only explicitly listed fields are accepted | |
| export const settingsFallbackStrategySchema = z.enum(ACCOUNT_FALLBACK_STRATEGY_VALUES); | |
| // Single source of truth: ../settingsSchemas (the schema the runtime settings route validates | |
| // against). Re-exported here so this modular barrel stays in exact lockstep β a divergent local | |
| // copy (introduced by the #3988 lossy modularization) silently dropped 40 fields while gaining a | |
| // few others. The settings-schema parity test guards this; see QUALITY_GATE_PLAYBOOK Parte 6 (G2). | |
| export { updateSettingsSchema } from "../settingsSchemas"; | |
| export const legacyResilienceProfileSchema = z.object({ | |
| transientCooldown: z.number().min(0), | |
| rateLimitCooldown: z.number().min(0), | |
| maxBackoffLevel: z.number().int().min(0), | |
| circuitBreakerThreshold: z.number().int().min(0), | |
| circuitBreakerReset: z.number().min(0), | |
| }); | |
| export const legacyResilienceDefaultsSchema = z | |
| .object({ | |
| requestsPerMinute: z.number().int().min(1).optional(), | |
| minTimeBetweenRequests: z.number().int().min(0).optional(), | |
| concurrentRequests: z.number().int().min(1).optional(), | |
| }) | |
| .strict(); | |
| export const requestQueueSettingsSchema = z | |
| .object({ | |
| autoEnableApiKeyProviders: z.boolean().optional(), | |
| requestsPerMinute: z.number().int().min(1).optional(), | |
| minTimeBetweenRequestsMs: z.number().int().min(0).optional(), | |
| concurrentRequests: z.number().int().min(1).optional(), | |
| maxWaitMs: z.number().int().min(1).optional(), | |
| }) | |
| .strict(); | |
| export const connectionCooldownProfileSchema = z | |
| .object({ | |
| baseCooldownMs: z.number().int().min(0).optional(), | |
| useUpstreamRetryHints: z.boolean().optional(), | |
| // Issue #2100 follow-up: per-profile toggle for upstream 429 hint trust. | |
| // `null` is an explicit unset sentinel β PATCH handler deletes the key | |
| // from stored settings so the per-provider default resolves at runtime. | |
| // `undefined` (key omitted) means "leave existing value unchanged". | |
| useUpstream429BreakerHints: z.boolean().nullable().optional(), | |
| maxBackoffSteps: z.number().int().min(0).optional(), | |
| }) | |
| .strict(); | |
| export const providerBreakerProfileSchema = z | |
| .object({ | |
| failureThreshold: z.number().int().min(1).max(1000).optional(), | |
| degradationThreshold: z.number().int().min(1).max(1000).optional(), | |
| resetTimeoutMs: z.number().int().min(1000).optional(), | |
| }) | |
| .strict() | |
| .superRefine((value, ctx) => { | |
| if ( | |
| typeof value.failureThreshold === "number" && | |
| value.failureThreshold > 1 && | |
| typeof value.degradationThreshold === "number" && | |
| value.degradationThreshold >= value.failureThreshold | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "degradationThreshold must be lower than failureThreshold", | |
| path: ["degradationThreshold"], | |
| }); | |
| } | |
| }); | |
| export const waitForCooldownSettingsSchema = z | |
| .object({ | |
| enabled: z.boolean().optional(), | |
| maxRetries: z.number().int().min(0).max(10).optional(), | |
| maxRetryWaitSec: z.number().int().min(0).max(300).optional(), | |
| }) | |
| .strict(); | |
| // Quota-share combo cooldown-aware retry (Variante A). Bounds mirror | |
| // normalizeComboCooldownWaitSettings: a single wait <= 30s, <= 10 attempts. | |
| export const comboCooldownWaitSettingsSchema = z | |
| .object({ | |
| enabled: z.boolean().optional(), | |
| maxWaitMs: z.number().int().min(0).max(30000).optional(), | |
| maxAttempts: z.number().int().min(0).max(10).optional(), | |
| budgetMs: z.number().int().min(0).max(300000).optional(), | |
| }) | |
| .strict(); | |
| // FASE 2.1: kill-switch for the per-connection quota-share concurrency limit. | |
| // The cap itself comes from each connection's max_concurrent, so only `enabled` | |
| // is configurable here. | |
| export const quotaShareConcurrencyLimitSettingsSchema = z | |
| .object({ | |
| enabled: z.boolean().optional(), | |
| }) | |
| .strict(); | |
| export const providerCooldownSettingsSchema = z | |
| .object({ | |
| enabled: z.boolean().optional(), | |
| minRetryCooldownMs: z.number().int().min(0).max(300000).optional(), | |
| maxRetryCooldownMs: z.number().int().min(0).max(3600000).optional(), | |
| }) | |
| .strict() | |
| .superRefine((value, ctx) => { | |
| if ( | |
| typeof value.minRetryCooldownMs === "number" && | |
| typeof value.maxRetryCooldownMs === "number" && | |
| value.maxRetryCooldownMs < value.minRetryCooldownMs | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "maxRetryCooldownMs must be greater than or equal to minRetryCooldownMs", | |
| path: ["maxRetryCooldownMs"], | |
| }); | |
| } | |
| }); | |
| export const updateResilienceSchema = z | |
| .object({ | |
| requestQueue: requestQueueSettingsSchema.optional(), | |
| connectionCooldown: z | |
| .object({ | |
| oauth: connectionCooldownProfileSchema.optional(), | |
| apikey: connectionCooldownProfileSchema.optional(), | |
| }) | |
| .strict() | |
| .optional(), | |
| providerBreaker: z | |
| .object({ | |
| oauth: providerBreakerProfileSchema.optional(), | |
| apikey: providerBreakerProfileSchema.optional(), | |
| }) | |
| .strict() | |
| .optional(), | |
| waitForCooldown: waitForCooldownSettingsSchema.optional(), | |
| comboCooldownWait: comboCooldownWaitSettingsSchema.optional(), | |
| quotaShareConcurrencyLimit: quotaShareConcurrencyLimitSettingsSchema.optional(), | |
| providerCooldown: providerCooldownSettingsSchema.optional(), | |
| profiles: z | |
| .object({ | |
| oauth: legacyResilienceProfileSchema.optional(), | |
| apikey: legacyResilienceProfileSchema.optional(), | |
| }) | |
| .strict() | |
| .optional(), | |
| defaults: legacyResilienceDefaultsSchema.optional(), | |
| }) | |
| .strict() | |
| .superRefine((value, ctx) => { | |
| if ( | |
| !value.requestQueue && | |
| !value.connectionCooldown && | |
| !value.providerBreaker && | |
| !value.waitForCooldown && | |
| !value.comboCooldownWait && | |
| !value.quotaShareConcurrencyLimit && | |
| !value.providerCooldown && | |
| !value.profiles && | |
| !value.defaults | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "Must provide resilience settings to update", | |
| path: [], | |
| }); | |
| } | |
| }); | |
| export const updateRequireLoginSchema = z | |
| .object({ | |
| requireLogin: z.boolean().optional(), | |
| password: z.string().min(4, "Password must be at least 4 characters").optional(), | |
| }) | |
| .superRefine((value, ctx) => { | |
| if (value.requireLogin === undefined && !value.password) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "No valid fields to update", | |
| path: [], | |
| }); | |
| } | |
| }); | |
| export const updateSystemPromptSchema = z | |
| .object({ | |
| prompt: z.string().max(50000).optional(), // legacy compat | |
| prefixPrompt: z.string().max(50000).optional(), | |
| suffixPrompt: z.string().max(50000).optional(), | |
| enabled: z.boolean().optional(), | |
| }) | |
| .strict() | |
| .superRefine((value, ctx) => { | |
| if ( | |
| value.prompt === undefined && | |
| value.prefixPrompt === undefined && | |
| value.suffixPrompt === undefined && | |
| value.enabled === undefined | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "No valid fields to update", | |
| path: [], | |
| }); | |
| } | |
| }); | |
| export const updateThinkingBudgetSchema = z | |
| .object({ | |
| mode: z.enum(["passthrough", "auto", "custom", "adaptive"]).optional(), | |
| customBudget: z.coerce.number().int().min(0).max(131072).optional(), | |
| effortLevel: z.enum(["none", "low", "medium", "high", "xhigh", "max"]).optional(), | |
| baseBudget: z.coerce.number().int().min(0).max(131072).optional(), | |
| complexityMultiplier: z.coerce.number().min(0).optional(), | |
| }) | |
| .strict() | |
| .superRefine((value, ctx) => { | |
| if ( | |
| value.mode === undefined && | |
| value.customBudget === undefined && | |
| value.effortLevel === undefined && | |
| value.baseBudget === undefined && | |
| value.complexityMultiplier === undefined | |
| ) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: "No valid fields to update", | |
| path: [], | |
| }); | |
| } | |
| }); | |
| export const guideSettingsSaveSchema = z | |
| .object({ | |
| baseUrl: z.string().trim().min(1).optional(), | |
| // #3552: the CLI tool cards post `apiKey: null` in cloud mode (the real key is resolved | |
| // server-side from keyId), and `z.string().optional()` rejected null β 400. Normalize | |
| // null β undefined so validation passes and the keyId/default path is used. | |
| apiKey: z.preprocess((v) => (v === null ? undefined : v), z.string().optional()), | |
| model: z.string().trim().min(1, "Model is required").optional(), | |
| models: z.array(z.string().trim().min(1, "Models must be non-empty")).min(1).optional(), | |
| modelLabels: z.record(z.string(), z.string().trim().min(1)).optional(), | |
| }) | |
| .refine((data) => !!data.model || !!data.models?.length, { | |
| message: "Model is required", | |
| path: ["model"], | |
| }); | |
| // βββ Auto-disable banned/error accounts βββββββββββββββββββββββββββββββββββ | |
| export const updateAutoDisableAccountsSchema = z | |
| .object({ | |
| enabled: z.boolean(), | |
| threshold: z.number().int().min(1).max(10).optional(), | |
| }) | |
| .strict(); | |