feat(*): 模型能力配置 - 支持模型级视频限制校验与协议管理

This commit is contained in:
ddCat
2026-08-06 15:34:45 +08:00
parent a18673a101
commit 9ca9dd9df6
36 changed files with 1692 additions and 401 deletions

View File

@@ -33,16 +33,17 @@ export function getAntThemeConfig(dark: boolean): ThemeConfig {
const elevatedBackground = dark ? "rgba(31, 31, 32, 0.96)" : "rgba(255, 255, 255, 0.96)";
const subtleBackground = dark ? "rgba(255, 255, 255, 0.055)" : "rgba(17, 17, 17, 0.035)";
const interactiveBorder = dark ? "rgba(255, 255, 255, 0.18)" : "rgba(17, 17, 17, 0.18)";
const focusShadow = dark ? "0 0 0 3px rgba(255, 255, 255, 0.12)" : "0 0 0 3px rgba(17, 17, 17, 0.08)";
// 黑白主题使用边框表达焦点,避免输入控件周围出现蓝紫色光圈。
const focusShadow = "none";
// 暗色界面的黑白主按钮与选择控件分色,避免开关轨道、勾选符号和滑块融成一片。
const selectionControl = dark
? {
active: "#4657c5",
active: "#ffffff",
border: "rgba(255, 255, 255, 0.3)",
disabledBackground: "rgba(255, 255, 255, 0.06)",
focus: "rgba(116, 133, 238, 0.28)",
hover: "#7485ee",
primary: "#5b6ee1",
focus: "transparent",
hover: "#e5e5e5",
primary: "#f5f5f5",
surface: "rgba(255, 255, 255, 0.035)",
}
: null;
@@ -180,7 +181,7 @@ export function getAntThemeConfig(dark: boolean): ThemeConfig {
multipleItemHeight: 24,
activeBorderColor: interactiveBorder,
hoverBorderColor: interactiveBorder,
activeOutlineColor: dark ? "rgba(255, 255, 255, 0.12)" : "rgba(17, 17, 17, 0.08)",
activeOutlineColor: "transparent",
optionActiveBg: color.selectActiveBg,
optionSelectedBg: color.selectSelectedBg,
optionSelectedColor: color.selectText,

View File

@@ -0,0 +1,121 @@
import type { ModelProtocol } from "@/lib/model-protocols";
export type ModelCapabilityConfig = {
version: number;
video?: VideoCapabilityConfig;
};
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;
};
export function defaultModelCapabilityConfig(protocol?: ModelProtocol): 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", "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 };
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") video.watermark = { supported: true, default: false };
return { version: 1, 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);
return cost?.capabilityConfig || defaultModelCapabilityConfig(cost?.protocol);
}
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 };
}
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;
}

View File

@@ -21,6 +21,7 @@ export async function testChannelModelConnection(channel: ModelChannel, model: s
protocol: testProtocol,
billingMode: modelCost?.billingMode || "fixed_request",
unitPriceMicrocredits: modelCost?.unitPriceMicrocredits || 0,
capabilityConfig: modelCost?.capabilityConfig,
},
...(channel.modelCosts || []).filter((item) => item.model !== model),
],