Files
ttstd-infinite-canvas/web/src/pages/admin/components/channel-model-editor-form.ts

106 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ModelCapabilityChoice } from "@/components/model-protocol-picker";
import { defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ModelCapabilityConfig } from "@/lib/model-capabilities";
import { modelProtocolSupportsTokenBilling, type ModelProtocolDefinition } from "@/lib/model-protocols";
import type { ChannelModel } from "@/services/api/wallet";
import type { ModelTag } from "@/lib/model-tags";
import { defaultPriceTier, legacyPriceTierToForm, priceTierToForm, type PriceTierFormValues } from "./channel-model-price-tier-form";
export type ChannelModelFormValues = {
modelKey: string;
providerModelKey?: string;
displayName?: string;
channelLabel?: string;
tags: ModelTag[];
description?: string;
icon?: string;
capability: ModelCapabilityChoice;
protocol?: string;
priceTiers: PriceTierFormValues[];
enabled: boolean;
capabilityConfig?: ModelCapabilityConfig;
};
export type EditorSection = "identity" | "capabilities" | "pricing";
export function editorSectionForField(name: (string | number)[]): EditorSection {
return name[0] === "priceTiers" ? "pricing" : name[0] === "capabilityConfig" ? "capabilities" : "identity";
}
export function initialChannelModelValues(item: ChannelModel | null, protocols: ModelProtocolDefinition[]): ChannelModelFormValues {
const capability = item?.capability || "text";
const protocol = item ? item.protocol : protocols.find((p) => p.capability === capability && p.enabled !== false)?.value;
const upstreamModel = item?.providerModelKey || item?.modelKey || "";
return {
modelKey: item?.modelKey || "",
providerModelKey: upstreamModel,
displayName: item?.displayName || "",
channelLabel: item?.channelLabel || "",
tags: item?.tags?.map((tag) => ({ ...tag })) || [],
description: item?.description || "",
icon: item?.icon || "",
capability,
protocol,
priceTiers: item ? (item.priceTiers?.length ? item.priceTiers.map(priceTierToForm) : [legacyPriceTierToForm(item)]) : [defaultPriceTier()],
enabled: item?.enabled ?? true,
capabilityConfig: capability === "audio" ? undefined : normalizeModelCapabilityConfig(item?.capabilityConfig || defaultModelCapabilityConfig(protocol, upstreamModel)),
};
}
// Clear incompatible selectors, but never silently convert prices or billing units.
export function changeChannelModelCapability(values: ChannelModelFormValues, protocols: ModelProtocolDefinition[]): ChannelModelFormValues {
const capability = values.capability;
const protocol = protocols.find((p) => p.value === values.protocol && p.capability === capability && p.enabled !== false)?.value || protocols.find((p) => p.capability === capability && p.enabled !== false)?.value;
return {
...values,
protocol,
capabilityConfig: capability === "audio" ? undefined : defaultModelCapabilityConfig(protocol, values.providerModelKey?.trim() || values.modelKey.trim()),
priceTiers: values.priceTiers.map((tier) => ({
...tier,
operation: "*",
quality: "*",
size: "*",
resolution: "*",
videoSeconds: 0,
videoGenerateAudio: "*",
imageCount: 0,
})),
};
}
export function validateChannelModelProtocol(capability: string, protocol: string | undefined, protocols: ModelProtocolDefinition[]) {
if (!protocols.some((p) => p.value === protocol && p.capability === capability && p.enabled !== false)) {
throw new Error("请选择当前能力下已启用的请求协议");
}
}
export function validateChannelModelPrices(values: Pick<ChannelModelFormValues, "capability" | "protocol" | "priceTiers">) {
const { capability, protocol, priceTiers } = values;
if (!priceTiers?.length) throw new Error("请至少配置一个价格档");
if (priceTiers.filter((tier) => tier.matchMode === "default").length > 1) throw new Error("只能配置一个所有规格统一价格");
const operations: Record<string, string[]> = {
text: ["text_generation"],
image: ["text_to_image", "image_to_image"],
video: ["text_to_video", "image_to_video", "video_to_video"],
audio: [],
};
priceTiers.forEach((tier, index) => {
const fail = (text: string) => {
throw new Error(`价格档 ${index + 1}:${text}`);
};
if (!["fixed_request", "per_second", "token"].includes(tier.billingMode)) fail("请选择计费方式");
if (tier.billingMode === "per_second" && capability !== "video") fail("按秒计费仅支持视频,请重新选择计费方式并核对价格");
if (tier.billingMode === "token" && !modelProtocolSupportsTokenBilling(capability, protocol)) fail("当前模型能力不支持 Token 计费,请重新选择计费方式并核对价格");
if (tier.matchMode === "advanced") {
if (tier.operation && tier.operation !== "*" && !operations[capability]?.includes(tier.operation)) fail("生成方式与模型能力不匹配");
const specific = (value: string | undefined) => Boolean(value && value !== "*");
if (!(specific(tier.operation) || (capability === "image" && (specific(tier.quality) || specific(tier.size))) || (capability === "video" && specific(tier.resolution)))) fail("规格价格至少需要一个匹配条件;统一价格请选择默认价格");
}
const prices = tier.billingMode !== "token" ? [tier.unitPrice] : capability === "video" ? [tier.outputTokenPrice] : [tier.inputTokenPrice, tier.outputTokenPrice, tier.cachedTokenPrice];
if (prices.some((price) => typeof price !== "number" || !Number.isFinite(price) || price < 0 || price > 1_000_000)) fail("积分价格必须是 0 到 1000000 之间的有效数值");
if (tier.costConfigured) {
const costs = tier.billingMode !== "token" ? [tier.costUnitPrice] : capability === "video" ? [tier.costOutputTokenPrice] : [tier.costInputTokenPrice, tier.costOutputTokenPrice, tier.costCachedTokenPrice];
if (costs.some((price) => typeof price !== "number" || !Number.isFinite(price) || price < 0 || price > 1_000_000)) fail("积分成本价必须是 0 到 1000000 之间的有效数值");
}
});
}