import type { ModelProtocol } from "@/lib/model-protocols"; export type ModelCapabilityConfig = { version: number; image?: ImageCapabilityConfig; video?: VideoCapabilityConfig; }; export type ImageSizeParameter = "none" | "size" | "aspect_ratio"; export type ImageCapabilityConfig = { references: { promptMaxChars: number; maxImages: number; maxImageBytes: number; maskSupported: boolean; }; size: { parameter: ImageSizeParameter; values: string[]; default: string; allowCustom: boolean; }; quality: { supported: boolean; values: string[]; default: string; }; transparentBackground: { supported: boolean; default: boolean }; responseFormat: { supported: boolean }; outputFormat: { supported: boolean }; maxOutputs: number; }; export type VideoCapabilityConfig = { references: { promptMaxChars: number; maxImages: number; maxImageBytes: number; maxVideos: number; maxVideoBytes: number; maxVideoDurationSeconds: number; maxAudios: number; maxAudioBytes: number; maxAudioDurationSeconds: number; }; duration: { selection: "range" | "enum"; min?: number; max?: number; step?: number; values?: number[]; default: number; }; ratios: string[]; defaultRatio: string; resolutions: string[]; defaultResolution: string; generateAudio: { supported: boolean; default: boolean }; watermark: { supported: boolean; default: boolean }; operations: string[]; defaultOperation: string; }; // Keep explicit pixel presets for each resolution tier so the settings panel can // switch between 1K, 2K and 4K without silently converting the requested ratio. const defaultImageSizes = [ "auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "21:9", "9:16", "1024x1024", "1360x1024", "1024x1360", "1536x1024", "1024x1536", "1024x1280", "1280x1024", "2048x878", "1824x1024", "1024x1824", "2048x2048", "2304x1728", "1728x2304", "2496x1664", "1664x2496", "1792x2240", "2240x1792", "3136x1344", "2752x1536", "1536x2752", "2880x2880", "3264x2448", "2448x3264", "3504x2336", "2336x3504", "2560x3200", "3200x2560", "3808x1632", "3840x2160", "2160x3840", ]; export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = ""): ImageCapabilityConfig { const image: ImageCapabilityConfig = { references: { promptMaxChars: 32000, maxImages: 16, maxImageBytes: 30 * 1024 * 1024, maskSupported: true }, size: { parameter: "size", values: [...defaultImageSizes], default: "1:1", allowCustom: true }, quality: { supported: true, values: ["auto", "low", "medium", "high"], default: "auto" }, transparentBackground: { supported: true, default: false }, responseFormat: { supported: true }, outputFormat: { supported: true }, maxOutputs: 15, }; if (protocol === "grok-image") { image.references.maxImages = 1; image.references.maskSupported = false; // grok2api / xAI Imagine:size→aspect_ratio,quality→resolution(1k/2k)。 image.size = { parameter: "aspect_ratio", values: ["1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"], default: "1:1", allowCustom: false, }; image.quality = { supported: true, values: ["1k", "2k"], default: "2k" }; image.transparentBackground = { supported: false, default: false }; image.responseFormat = { supported: true }; image.outputFormat = { supported: false }; image.maxOutputs = 1; } else if (protocol === "volcengine-ark-image") { image.references.maskSupported = false; image.quality.supported = false; image.transparentBackground.supported = false; image.responseFormat.supported = false; image.outputFormat.supported = false; } if (protocol === "volcengine-jimeng-image") { image.references.maxImages = 14; image.references.maskSupported = false; image.quality.supported = false; image.transparentBackground.supported = false; image.responseFormat.supported = false; image.outputFormat.supported = false; } if (protocol !== "grok-image" && model.trim().toLowerCase().startsWith("grok-imagine-image")) { image.references.maxImages = 0; image.references.maskSupported = false; image.size = { parameter: "aspect_ratio", values: ["1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2"], default: "1:1", allowCustom: false, }; image.quality = { supported: true, values: ["1k", "2k"], default: "2k" }; image.transparentBackground = { supported: false, default: false }; image.responseFormat = { supported: true }; image.outputFormat = { supported: false }; image.maxOutputs = 1; } return image; } export function defaultModelCapabilityConfig(protocol?: ModelProtocol, model = ""): ModelCapabilityConfig { const video: VideoCapabilityConfig = { references: { promptMaxChars: 1000, maxImages: 9, maxImageBytes: 30 * 1024 * 1024, maxVideos: 0, maxVideoBytes: 0, maxVideoDurationSeconds: 0, maxAudios: 0, maxAudioBytes: 0, maxAudioDurationSeconds: 0, }, duration: { selection: "range", min: 1, max: 15, step: 1, default: 6 }, ratios: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"], defaultRatio: "16:9", resolutions: ["480p", "720p", "1080p", "1440p", "2160p"], defaultResolution: "720p", generateAudio: { supported: false, default: false }, watermark: { supported: false, default: false }, operations: ["text_to_video", "image_to_video"], defaultOperation: "text_to_video", }; if (protocol === "volcengine-jimeng-video") { video.duration = { selection: "enum", values: [5, 10], default: 5 }; video.resolutions = ["720p"]; } if (protocol === "gemini-veo") { video.duration = { selection: "enum", values: [4, 6, 8], default: 6 }; video.resolutions = ["720p", "1080p"]; } if (protocol === "volcengine-ark-video" || protocol === "newapi-channel-1" || protocol === "newapi-channel-2") { video.references.maxVideos = 3; video.references.maxAudios = 3; video.references.maxVideoBytes = 200 * 1024 * 1024; video.references.maxAudioBytes = 15 * 1024 * 1024; video.references.maxVideoDurationSeconds = 15; video.references.maxAudioDurationSeconds = 15; video.generateAudio = { supported: true, default: true }; } if (protocol === "volcengine-ark-video" || protocol === "newapi-channel-1") video.resolutions = ["480p", "720p", "1080p"]; if (protocol === "volcengine-ark-video") video.watermark = { supported: true, default: false }; if (protocol === "novita-video") { video.references.maxImages = 1; video.references.maxImageBytes = 10 * 1024 * 1024; video.duration = { selection: "enum", values: [5, 10], default: 5 }; video.ratios = ["16:9", "9:16", "1:1"]; video.resolutions = ["1080p"]; video.defaultResolution = "1080p"; } return { version: 1, image: defaultImageCapabilityConfig(protocol, model), video }; } export function modelCapabilityConfigFor(config: { channels: Array<{ id: string; models: string[]; modelCosts?: Array<{ model: string; capabilityConfig?: ModelCapabilityConfig; protocol?: ModelProtocol }> }> }, model: string) { const separator = model.indexOf("::"); const channelId = separator >= 0 ? model.slice(0, separator) : ""; const modelName = separator >= 0 ? model.slice(separator + 2) : model; const channel = config.channels.find((item) => item.id === channelId) || config.channels.find((item) => item.models.includes(modelName)); const cost = channel?.modelCosts?.find((item) => item.model === modelName); const fallback = defaultModelCapabilityConfig(cost?.protocol, modelName); return cost?.capabilityConfig ? { ...fallback, ...cost.capabilityConfig, image: cost.capabilityConfig.image || fallback.image, video: cost.capabilityConfig.video || fallback.video } : fallback; } export function normalizeImageValue(profile: ImageCapabilityConfig, value: { size?: string; quality?: string; count?: string; transparentBackground?: string }) { const size = normalizeImageSizeSetting(profile, value.size); const quality = profile.quality.supported ? (value.quality && profile.quality.values.includes(value.quality) ? value.quality : profile.quality.default || "auto") : profile.quality.default || "auto"; const count = String(Math.max(1, Math.min(profile.maxOutputs, Math.floor(Math.abs(Number(value.count)) || 1)))); const transparentBackground = profile.transparentBackground.supported && value.transparentBackground === "true" ? "true" : "false"; return { size, quality, count, transparentBackground }; } export function normalizeImageSizeSetting(profile: ImageCapabilityConfig, value?: string) { if (profile.size.parameter === "none") return "auto"; const candidate = value?.trim() || profile.size.default; if (profile.size.allowCustom || profile.size.values.includes(candidate)) return candidate; return profile.size.default || profile.size.values[0] || "auto"; } export function imageSizeRequest(profile: ImageCapabilityConfig, value?: string) { const parameter = profile.size.parameter; if (parameter === "none") return undefined; const normalized = normalizeImageSizeSetting(profile, value); if (!normalized || normalized === "auto") return undefined; return { parameter, value: normalized }; } export function normalizeVideoValue(profile: VideoCapabilityConfig, value: { seconds?: string; ratio?: string; resolution?: string }) { const duration = profile.duration.selection === "enum" ? (profile.duration.values || []).includes(Number(value.seconds)) ? Number(value.seconds) : profile.duration.default : normalizeRangeDuration(profile, Number(value.seconds)); const ratio = profile.ratios.includes(value.ratio || "") ? value.ratio! : profile.defaultRatio; const resolution = profile.resolutions.includes(value.resolution || "") ? value.resolution! : profile.defaultResolution; return { seconds: String(duration), ratio, resolution }; } export function videoResolutionRequest(profile: VideoCapabilityConfig, value: string | undefined) { const requested = String(value || "").trim().toLowerCase(); if (!requested || requested === "auto" || requested === "default" || requested === "medium" || requested === "high") return undefined; const candidates = [requested]; if (/^\d+$/.test(requested)) candidates.push(`${requested}p`); if (requested === "low") candidates.push("480p"); if (requested === "4k") candidates.push("2160p"); if (requested === "2160" || requested === "2160p") candidates.push("4k"); const supported = new Map(profile.resolutions.map((resolution) => [resolution.trim().toLowerCase(), resolution.trim()])); for (const candidate of candidates) { const match = supported.get(candidate); if (match) return match; } return undefined; } function normalizeRangeDuration(profile: VideoCapabilityConfig, value: number) { const min = profile.duration.min || 1; const max = profile.duration.max || min; const step = profile.duration.step || 1; const candidate = Number.isFinite(value) ? Math.floor(value) : profile.duration.default; const clamped = Math.min(max, Math.max(min, candidate)); const maxStep = Math.max(0, Math.floor((max - min) / step)); return min + Math.min(maxStep, Math.max(0, Math.round((clamped - min) / step))) * step; } export function videoDurationOptions(profile: VideoCapabilityConfig) { if (profile.duration.selection === "enum") return profile.duration.values || []; const min = profile.duration.min || 1; const max = profile.duration.max || min; const step = profile.duration.step || 1; return Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, index) => min + index * step); } export function videoDurationAllowed(profile: VideoCapabilityConfig, value: number) { if (profile.duration.selection === "enum") return (profile.duration.values || []).includes(value); const min = profile.duration.min || 1; const max = profile.duration.max || min; const step = profile.duration.step || 1; return value >= min && value <= max && (value - min) % step === 0; }