From e124b50922fa1841ec8a5bf18531e2230b64a230 Mon Sep 17 00:00:00 2001 From: Jan Leon Date: Tue, 30 Jun 2026 02:58:30 +0200 Subject: [PATCH] chore(duplication): share provider specific validation (#5485) --- src/shared/validation/schemas/provider.ts | 312 +----------------- .../unit/bailian-coding-plan-provider.test.ts | 22 ++ 2 files changed, 24 insertions(+), 310 deletions(-) diff --git a/src/shared/validation/schemas/provider.ts b/src/shared/validation/schemas/provider.ts index 27d0a32648..3f8f06bd48 100644 --- a/src/shared/validation/schemas/provider.ts +++ b/src/shared/validation/schemas/provider.ts @@ -7,329 +7,21 @@ 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 { validateProviderSpecificData } from "@/shared/validation/providerSpecificData"; import { - isHttpUrl, - CODEX_REASONING_EFFORT_VALUES, - REQUEST_DEFAULT_SERVICE_TIER_VALUES, upstreamHeadersRecordSchema, modelCompatPerProtocolSchema, customHeadersSchema, } from "./misc.ts"; -export function validateProviderSpecificData( - data: Record | 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; - 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"], - }); - } - } -} +export { validateProviderSpecificData }; // ──── Provider Schemas ──── diff --git a/tests/unit/bailian-coding-plan-provider.test.ts b/tests/unit/bailian-coding-plan-provider.test.ts index c0de35dd19..2492071a86 100644 --- a/tests/unit/bailian-coding-plan-provider.test.ts +++ b/tests/unit/bailian-coding-plan-provider.test.ts @@ -124,6 +124,28 @@ test("createProviderSchema rejects baseUrl with non-string value", () => { assert.equal(validation.success, false, "Should reject non-string baseUrl"); }); +test("createProviderSchema rejects non-boolean Codex context1m request default", () => { + const validation = validateBody(createProviderSchema, { + provider: "codex", + apiKey: "sk-test-key", + name: "Test Codex", + providerSpecificData: { + requestDefaults: { + context1m: "yes", + }, + }, + }); + + assert.equal(validation.success, false, "Should reject non-boolean context1m"); + if (!validation.success && typeof validation.error === "object" && validation.error !== null) { + const details = Array.isArray(validation.error.details) ? validation.error.details : []; + assert.ok( + details.some((detail) => String(detail.message || "").includes("context1m")), + "Error should mention context1m" + ); + } +}); + test("updateProviderConnectionSchema accepts valid baseUrl in providerSpecificData", () => { const validation = validateBody(updateProviderConnectionSchema, { providerSpecificData: {