refactor(admin): 模型定价 - 重排规格价格编辑并精简视频匹配条件
This commit is contained in:
@@ -90,9 +90,7 @@ export function validateChannelModelPrices(values: Pick<ChannelModelFormValues,
|
||||
if (tier.matchMode === "advanced") {
|
||||
if (tier.operation && tier.operation !== "*" && !operations[capability]?.includes(tier.operation)) fail("生成方式与模型能力不匹配");
|
||||
const specific = (value: string | undefined) => Boolean(value && value !== "*");
|
||||
if (capability === "video" && tier.videoGenerateAudio && !["*", "true", "false"].includes(tier.videoGenerateAudio)) fail("请选择任意音频、有声或无声");
|
||||
if (!(specific(tier.operation) || (capability === "image" && (specific(tier.quality) || specific(tier.size))) || (capability === "video" && (specific(tier.resolution) || tier.videoSeconds > 0 || tier.imageCount > 0 || specific(tier.videoGenerateAudio)))))
|
||||
fail("规格价格至少需要一个匹配条件;统一价格请选择默认价格");
|
||||
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 之间的有效数值");
|
||||
|
||||
@@ -372,27 +372,31 @@ export function ChannelModelEditor({
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<div className="space-y-3">
|
||||
{fields.map((field, index) => (
|
||||
<PriceTierFields
|
||||
key={field.key}
|
||||
index={field.name}
|
||||
ordinal={index + 1}
|
||||
form={form}
|
||||
capability={modelCapability}
|
||||
protocol={modelProtocol}
|
||||
capabilityConfig={capabilityConfig}
|
||||
modelUpstream={modelUpstream}
|
||||
onDirty={() => {
|
||||
dirtyRef.current = true;
|
||||
}}
|
||||
onRemove={() => remove(field.name)}
|
||||
/>
|
||||
))}
|
||||
<Button className="admin-model-editor-add-tier" type="dashed" block icon={<Plus className="size-4" />} onClick={() => add(defaultPriceTier(hasDefaultPriceTier ? "advanced" : "default"))}>
|
||||
{hasDefaultPriceTier ? "新增规格价格" : "新增统一默认价格"}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors} />
|
||||
<div className="admin-price-tier-list-shell">
|
||||
<div className="admin-price-tier-list" aria-label="积分价格规则列表">
|
||||
{fields.map((field, index) => (
|
||||
<PriceTierFields
|
||||
key={field.key}
|
||||
index={field.name}
|
||||
ordinal={index + 1}
|
||||
form={form}
|
||||
capability={modelCapability}
|
||||
protocol={modelProtocol}
|
||||
capabilityConfig={capabilityConfig}
|
||||
modelUpstream={modelUpstream}
|
||||
onDirty={() => {
|
||||
dirtyRef.current = true;
|
||||
}}
|
||||
onRemove={() => remove(field.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="admin-price-tier-list-footer">
|
||||
<Button className="admin-model-editor-add-tier" type="dashed" block icon={<Plus className="size-4" />} onClick={() => add(defaultPriceTier(hasDefaultPriceTier ? "advanced" : "default"))}>
|
||||
{hasDefaultPriceTier ? "新增规格价格" : "新增统一默认价格"}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
@@ -38,23 +38,26 @@ export function PriceTierFields({
|
||||
const staleTierUpstream = matchMode === "default" && tierUpstream && modelUpstream && tierUpstream !== modelUpstream ? tierUpstream : "";
|
||||
const video = capabilityConfig?.video;
|
||||
const resolutionOptions = video?.resolutions || [];
|
||||
const durationOptions = video?.duration.selection === "enum" ? video.duration.values || [] : [];
|
||||
const tokenEnabled = modelProtocolSupportsTokenBilling(capability, protocol);
|
||||
const isVideo = capability === "video";
|
||||
const isImage = capability === "image";
|
||||
return (
|
||||
<div className="admin-price-tier-card">
|
||||
<div className="admin-price-tier-card-header">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{matchMode === "default" ? "默认价格" : `规格价格 ${ordinal}`}</div>
|
||||
<article className="admin-price-tier-card">
|
||||
<header className="admin-price-tier-card-header">
|
||||
<Form.Item name={[index, "priceConfigured"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={[index, "enabled"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<div className="admin-price-tier-card-heading">
|
||||
<span className="admin-price-tier-card-index">{String(ordinal).padStart(2, "0")}</span>
|
||||
<div className="admin-price-tier-card-heading-copy">
|
||||
<div className="admin-price-tier-card-title">{matchMode === "default" ? "默认价格" : `规格价格 ${ordinal}`}</div>
|
||||
<div className="admin-price-tier-card-summary">{matchMode === "default" ? "所有请求使用同一价格" : isVideo ? "按生成方式与分辨率匹配" : "按生成方式、质量或尺寸匹配"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-price-tier-controls">
|
||||
<Form.Item name={[index, "priceConfigured"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={[index, "enabled"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<div className="admin-price-tier-card-actions">
|
||||
<div className="admin-price-tier-toggle">
|
||||
<span title="关闭后保留配置,但不参与用户请求及计费匹配">可供用户使用</span>
|
||||
<Switch
|
||||
@@ -67,116 +70,125 @@ export function PriceTierFields({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button type="text" danger aria-label={`删除价格档 ${ordinal}`} icon={<Trash2 className="size-3.5" />} onClick={onRemove}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="text" danger aria-label={`删除价格档 ${ordinal}`} icon={<Trash2 className="size-3.5" />} onClick={onRemove}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="admin-price-tier-card-body">
|
||||
<div className="admin-price-tier-block admin-price-tier-match-block">
|
||||
<Form.Item className="mb-0" name={[index, "matchMode"]} label="定价方式" tooltip="默认价格适用于所有请求;按规格配置时,精确规则优先。" rules={[{ required: true }]}>
|
||||
<section className="admin-price-tier-mode-row">
|
||||
<div className="admin-price-tier-panel-heading">
|
||||
<div>
|
||||
<h3>适用范围</h3>
|
||||
<p>选择全部请求,或按生成规格精确匹配。</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item className="admin-price-tier-mode-control mb-0" name={[index, "matchMode"]} rules={[{ required: true }]}>
|
||||
<Segmented
|
||||
block
|
||||
aria-label="价格规则适用范围"
|
||||
options={[
|
||||
{ label: "统一价格", value: "default" },
|
||||
{ label: "按规格定价", value: "advanced" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="admin-price-tier-block admin-price-tier-billing-block">
|
||||
<div className="mb-2 text-sm font-medium">销售价格</div>
|
||||
<div className="admin-price-tier-billing-grid">
|
||||
<Form.Item className="admin-price-tier-billing-mode mb-0" name={[index, "billingMode"]} label="计费方式" rules={[{ required: true }]}>
|
||||
<Segmented
|
||||
className="w-full"
|
||||
options={[
|
||||
{ label: "按次", value: "fixed_request" },
|
||||
{ label: "按秒", value: "per_second", disabled: !isVideo },
|
||||
{ label: isVideo ? "视频 Token" : "Token", value: "token", disabled: !tokenEnabled },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{billingMode === "token" ? (
|
||||
isVideo ? (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "outputTokenPrice"]} label="积分 / 百万视频 Token" rules={[{ required: true, message: "请输入视频 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="admin-price-tier-token-grid">
|
||||
<Form.Item className="mb-0" name={[index, "inputTokenPrice"]} label="输入 / 百万 Token" rules={[{ required: true, message: "请输入输入价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="mb-0" name={[index, "outputTokenPrice"]} label="输出 / 百万 Token" rules={[{ required: true, message: "请输入输出价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="mb-0" name={[index, "cachedTokenPrice"]} label="缓存 / 百万 Token" rules={[{ required: true, message: "请输入缓存价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
</section>
|
||||
<div className={`admin-price-tier-workspace ${matchMode === "default" ? "is-default" : "is-advanced"}`}>
|
||||
{matchMode !== "default" && (
|
||||
<section className="admin-price-tier-panel admin-price-tier-conditions-panel">
|
||||
<div className="admin-price-tier-panel-heading">
|
||||
<div>
|
||||
<h3>匹配条件</h3>
|
||||
<p>符合以下条件时使用这条价格,精确规则优先。</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "unitPrice"]} label={billingMode === "per_second" ? "每秒消耗积分" : "每次消耗积分"} rules={[{ required: true, message: "请输入积分价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
{isVideo && billingMode === "token" ? (
|
||||
<p className="mb-0 mt-2 text-xs text-foreground/60">
|
||||
所有视频模型均支持 Token 计费,统一按火山引擎模式计算:宽 × 高 × 24 帧/秒 ×(输出时长 + 参考视频时长)÷ 1024。优先按成功任务返回的有效用量结算,未返回用量时按公式结算。平台额外预留 10%,不计入公式结算;最终可能补扣或退回差额。0 表示免费;有声、无声或参考视频可分别配置规格价格。
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<CreditCostFields index={index} form={form} billingMode={billingMode} isVideo={isVideo} />
|
||||
{matchMode !== "default" && (
|
||||
<div className="admin-price-tier-match-grid">
|
||||
<Form.Item className="mb-0" name={[index, "operation"]} label="生成方式" rules={[{ required: true, message: "请选择生成方式" }]}>
|
||||
<Select options={operationOptions(capability)} />
|
||||
</Form.Item>
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "resolution"]} label="分辨率" rules={[{ required: true, message: "请选择分辨率" }]}>
|
||||
<Select options={[{ label: "任意分辨率", value: "*" }, ...resolutionOptions.map((value) => ({ label: value.toUpperCase(), value }))]} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "videoSeconds"]} label="时长" rules={[{ required: true, message: "请输入时长" }]}>
|
||||
{durationOptions.length ? <Select options={[{ label: "任意时长", value: 0 }, ...durationOptions.map((value) => ({ label: `${value} 秒`, value }))]} /> : <InputNumber className="w-full" min={0} precision={0} />}
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "videoGenerateAudio"]} label="生成音频" rules={[{ required: true, message: "请选择音频条件" }]}>
|
||||
<Select options={[{ label: "任意音频", value: "*" }, { label: "有声", value: "true" }, { label: "无声", value: "false" }]} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "imageCount"]} label="参考图数量" rules={[{ required: true, message: "请输入参考图数量" }]}>
|
||||
<InputNumber className="w-full" min={0} max={9} precision={0} placeholder="0 表示任意数量" />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "quality"]} label="质量/分辨率" rules={[{ required: true, message: "请选择质量或分辨率" }]}>
|
||||
<Select
|
||||
</div>
|
||||
<div className="admin-price-tier-match-grid">
|
||||
<Form.Item className="mb-0" name={[index, "operation"]} label="生成方式" rules={[{ required: true, message: "请选择生成方式" }]}>
|
||||
<Select options={operationOptions(capability)} />
|
||||
</Form.Item>
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "resolution"]} label="分辨率" rules={[{ required: true, message: "请选择分辨率" }]}>
|
||||
<Select options={[{ label: "任意分辨率", value: "*" }, ...resolutionOptions.map((value) => ({ label: value.toUpperCase(), value }))]} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "quality"]} label="质量/分辨率" rules={[{ required: true, message: "请选择质量或分辨率" }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "任意质量", value: "*" },
|
||||
{ label: "1K", value: "1k" },
|
||||
{ label: "2K", value: "2k" },
|
||||
{ label: "4K", value: "4k" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "size"]} label="画幅/尺寸">
|
||||
<Input placeholder="任意,或 1:1、16:9、1024x1024" />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item className="admin-price-tier-upstream mb-0" name={[index, "providerModelKey"]} label="命中后使用的上游模型 ID">
|
||||
<Input placeholder="留空则使用模型默认上游 ID" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section className="admin-price-tier-panel admin-price-tier-selling-panel">
|
||||
<div className="admin-price-tier-panel-heading">
|
||||
<div>
|
||||
<h3>销售价格</h3>
|
||||
<p>设置用户完成一次生成实际消耗的积分。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-price-tier-billing-grid">
|
||||
<Form.Item className="admin-price-tier-billing-mode mb-0" name={[index, "billingMode"]} label="计费方式" rules={[{ required: true }]}>
|
||||
<Segmented
|
||||
className="w-full"
|
||||
options={[
|
||||
{ label: "任意质量", value: "*" },
|
||||
{ label: "1K", value: "1k" },
|
||||
{ label: "2K", value: "2k" },
|
||||
{ label: "4K", value: "4k" },
|
||||
{ label: "按次", value: "fixed_request" },
|
||||
{ label: "按秒", value: "per_second", disabled: !isVideo },
|
||||
{ label: isVideo ? "视频 Token" : "Token", value: "token", disabled: !tokenEnabled },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{billingMode === "token" ? (
|
||||
isVideo ? (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "outputTokenPrice"]} label="积分 / 百万视频 Token" rules={[{ required: true, message: "请输入视频 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="admin-price-tier-token-grid">
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "inputTokenPrice"]} label="输入 / 百万 Token" rules={[{ required: true, message: "请输入输入 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "outputTokenPrice"]} label="输出 / 百万 Token" rules={[{ required: true, message: "请输入输出 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "cachedTokenPrice"]} label="缓存 / 百万 Token" rules={[{ required: true, message: "请输入缓存 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "unitPrice"]} label={billingMode === "per_second" ? "每秒消耗积分" : "每次消耗积分"} rules={[{ required: true, message: "请输入积分价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
{isVideo && billingMode === "token" ? (
|
||||
<div className="admin-price-tier-helper" role="note">
|
||||
<strong>视频 Token 计费</strong>
|
||||
<span>估算:宽 × 高 × 24 帧/秒 ×(输出时长 + 参考视频时长)÷ 1024;优先按上游有效用量结算,无用量时使用公式,授权预留 10% 会在结算后补扣或退回。</span>
|
||||
</div>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "size"]} label="画幅/尺寸">
|
||||
<Input placeholder="任意,或 1:1、16:9、1024x1024" />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item className="admin-price-tier-upstream mb-0" name={[index, "providerModelKey"]} label="命中后使用的上游模型 ID">
|
||||
<Input placeholder="留空则使用模型默认上游 ID" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
<CreditCostFields index={index} form={form} billingMode={billingMode} isVideo={isVideo} />
|
||||
</section>
|
||||
</div>
|
||||
{staleTierUpstream ? (
|
||||
<Alert
|
||||
className="admin-price-tier-alert"
|
||||
type="warning"
|
||||
showIcon
|
||||
title="该统一价格档仍指向独立上游模型 ID"
|
||||
@@ -195,7 +207,7 @@ export function PriceTierFields({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ export type PriceTierMatchMode = "default" | "advanced";
|
||||
|
||||
// 与后端保存口径一致:上游键比较前剥掉一次 models/ 前缀,避免极旧存量行误报差异。
|
||||
export function normalizeUpstreamModelKey(value: unknown): string {
|
||||
return String(value || "").trim().replace(/^models\//, "");
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.replace(/^models\//, "");
|
||||
}
|
||||
|
||||
export type PriceTierFormValues = {
|
||||
@@ -60,7 +62,7 @@ export function defaultPriceTier(matchMode: PriceTierMatchMode = "default"): Pri
|
||||
|
||||
export function priceTierToForm(tier: ChannelModelPriceTier): PriceTierFormValues {
|
||||
const selector = tier.selector || {};
|
||||
const hasSpecificMatch = Object.values(selector).some((value) => value && value !== "*") || (tier.resolution && tier.resolution !== "*") || tier.videoSeconds > 0;
|
||||
const hasSpecificMatch = [selector.operation, selector.quality, selector.size].some((value) => value && value !== "*") || (tier.resolution && tier.resolution !== "*");
|
||||
return {
|
||||
matchMode: hasSpecificMatch ? "advanced" : "default",
|
||||
operation: selector.operation || "*",
|
||||
@@ -107,9 +109,6 @@ export function skuSelectorFromForm(capability: ModelCapabilityChoice, tier: Pri
|
||||
if (tier.operation && tier.operation !== "*") selector.operation = tier.operation;
|
||||
if (capability === "video") {
|
||||
if (tier.resolution && tier.resolution !== "*") selector.vquality = tier.resolution;
|
||||
if (Number(tier.videoSeconds) > 0) selector.videoSeconds = String(Number(tier.videoSeconds));
|
||||
if (Number(tier.imageCount) > 0) selector.imageCount = String(Number(tier.imageCount));
|
||||
if (tier.videoGenerateAudio && tier.videoGenerateAudio !== "*") selector.videoGenerateAudio = tier.videoGenerateAudio;
|
||||
}
|
||||
if (capability === "image") {
|
||||
if (tier.quality && tier.quality !== "*") selector.quality = tier.quality;
|
||||
@@ -123,7 +122,9 @@ export function priceTierResolutionFromForm(capability: ModelCapabilityChoice, t
|
||||
}
|
||||
|
||||
export function priceTierVideoSecondsFromForm(capability: ModelCapabilityChoice, tier: PriceTierFormValues) {
|
||||
return capability === "video" && tier.matchMode === "advanced" ? Number(tier.videoSeconds || 0) : 0;
|
||||
void capability;
|
||||
void tier;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function priceTierPayloadFromForm(capability: ModelCapabilityChoice, tier: PriceTierFormValues, upstreamModel: string) {
|
||||
|
||||
@@ -14,12 +14,21 @@ export function CreditCostFields({ index, form, billingMode, isVideo }: { index:
|
||||
]
|
||||
: [["costUnitPrice", billingMode === "per_second" ? "积分 / 秒" : "积分 / 次"]];
|
||||
return (
|
||||
<div className="admin-price-tier-block">
|
||||
<Form.Item name={[index, "costConfigured"]} label="积分成本价(仅管理员可见)" valuePropName="checked" extra="按当前价格档的计费方式核算成本,不影响用户售价;开启并填 0 表示零成本。">
|
||||
<Switch aria-label="配置积分成本价" />
|
||||
</Form.Item>
|
||||
<section className="admin-price-tier-cost-panel" aria-label="积分成本设置">
|
||||
<header className="admin-price-tier-cost-header">
|
||||
<div className="admin-price-tier-cost-heading">
|
||||
<div className="admin-price-tier-cost-title">
|
||||
<span>积分成本</span>
|
||||
<span className="admin-price-tier-cost-badge">仅管理员可见</span>
|
||||
</div>
|
||||
<p>按当前计费方式填写成本,不影响用户售价。</p>
|
||||
</div>
|
||||
<Form.Item className="admin-price-tier-cost-switch mb-0" name={[index, "costConfigured"]} valuePropName="checked">
|
||||
<Switch aria-label="配置积分成本价" />
|
||||
</Form.Item>
|
||||
</header>
|
||||
{configured ? (
|
||||
<div className={billingMode === "token" && !isVideo ? "admin-price-tier-token-grid" : "admin-price-tier-billing-grid"}>
|
||||
<div className={billingMode === "token" && !isVideo ? "admin-price-tier-cost-grid admin-price-tier-cost-token-grid" : "admin-price-tier-cost-grid"}>
|
||||
{fields.map(([field, label]) => (
|
||||
<Form.Item key={field} className="mb-0" name={[index, field]} label={label} rules={[{ required: true, message: "请输入积分成本价" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
@@ -27,6 +36,6 @@ export function CreditCostFields({ index, form, billingMode, isVideo }: { index:
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
||||
capability: nextCapability,
|
||||
billingMode: "fixed_request",
|
||||
unitPriceMicrocredits: 0,
|
||||
capabilityConfig: nextCapability === "image" || nextCapability === "video" ? defaultModelCapabilityConfig(nextProtocol, activeModel) : undefined,
|
||||
capabilityConfig: nextCapability === "text" || nextCapability === "image" || nextCapability === "video" ? defaultModelCapabilityConfig(nextProtocol, activeModel) : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -165,7 +165,7 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
||||
protocol: nextProtocol,
|
||||
billingMode: "fixed_request",
|
||||
unitPriceMicrocredits: 0,
|
||||
capabilityConfig: activeCapability === "image" || activeCapability === "video" ? defaultModelCapabilityConfig(nextProtocol, activeModel) : undefined,
|
||||
capabilityConfig: activeCapability === "text" || activeCapability === "image" || activeCapability === "video" ? defaultModelCapabilityConfig(nextProtocol, activeModel) : undefined,
|
||||
})}
|
||||
/>
|
||||
</section>
|
||||
@@ -175,7 +175,7 @@ export function ChannelModelSettings({ channel, onChange }: { channel: ModelChan
|
||||
key: "capabilities",
|
||||
label: "能力与参数",
|
||||
children: <div inert={Boolean(testingModel)}>
|
||||
{activeCapability === "image" || activeCapability === "video" ? (
|
||||
{activeCapability === "text" || activeCapability === "image" || activeCapability === "video" ? (
|
||||
<ModelCapabilityEditor
|
||||
capability={activeCapability}
|
||||
model={activeModel}
|
||||
|
||||
@@ -9263,11 +9263,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.admin-price-tier-list-shell {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-price-tier-list {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-price-tier-list-footer {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-price-tier-card {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 8%, var(--admin-card-border));
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--foreground) 2%, var(--admin-layer-1));
|
||||
background: var(--admin-layer-1);
|
||||
box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 4%, transparent);
|
||||
}
|
||||
|
||||
.admin-price-tier-card-header {
|
||||
@@ -9275,86 +9295,274 @@
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 9%, transparent);
|
||||
gap: 10px;
|
||||
background: color-mix(in srgb, var(--foreground) 1.5%, var(--admin-layer-1));
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-heading,
|
||||
.admin-price-tier-card-actions,
|
||||
.admin-price-tier-cost-header,
|
||||
.admin-price-tier-cost-title {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-heading {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-index {
|
||||
display: grid;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--workspace-accent) 10%, var(--admin-layer-2));
|
||||
color: color-mix(in srgb, var(--workspace-accent) 76%, var(--admin-text-primary));
|
||||
font-size: 10px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-heading-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-title {
|
||||
color: var(--admin-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-summary {
|
||||
overflow: hidden;
|
||||
color: var(--admin-text-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-actions {
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-actions > .ant-btn {
|
||||
padding-inline: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-price-tier-card-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
|
||||
gap: 12px 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.admin-price-tier-block {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-price-tier-block-title {
|
||||
margin-bottom: 10px;
|
||||
color: color-mix(in srgb, var(--foreground) 48%, transparent);
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.admin-price-tier-block-title > span {
|
||||
margin-right: 5px;
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
|
||||
.admin-price-tier-match-grid {
|
||||
grid-column: 1 / -1;
|
||||
.admin-price-tier-mode-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: color-mix(in srgb, var(--foreground) 0.8%, transparent);
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.admin-price-tier-upstream {
|
||||
grid-column: span 2;
|
||||
.admin-price-tier-mode-row .admin-price-tier-panel-heading {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admin-price-tier-billing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
.admin-price-tier-mode-row .admin-price-tier-panel-heading > div {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 8px;
|
||||
}
|
||||
|
||||
.admin-price-tier-token-grid {
|
||||
grid-column: 1 / -1;
|
||||
.admin-price-tier-mode-row .admin-price-tier-panel-heading p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.admin-price-tier-token-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
.admin-price-tier-mode-control {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-price-tier-billing-mode .ant-segmented {
|
||||
border: 1px solid var(--admin-card-border);
|
||||
.admin-price-tier-mode-control .ant-segmented {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: var(--admin-layer-2);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.admin-price-tier-billing-mode .ant-segmented-item {
|
||||
min-height: 28px;
|
||||
border-radius: 5px;
|
||||
.admin-price-tier-workspace {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.admin-price-tier-billing-mode .ant-segmented-item-label {
|
||||
min-height: 28px;
|
||||
padding-inline: 12px;
|
||||
line-height: 28px;
|
||||
.admin-price-tier-workspace.is-advanced {
|
||||
grid-template-columns: minmax(320px, 0.9fr) minmax(0, 1.6fr);
|
||||
}
|
||||
|
||||
.admin-price-tier-billing-mode .ant-segmented-item-selected {
|
||||
background: var(--admin-layer-1);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--foreground) 14%, transparent);
|
||||
.admin-price-tier-workspace.is-default {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.admin-price-tier-unit-price .ant-input-number {
|
||||
.admin-price-tier-panel {
|
||||
min-width: 0;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--foreground) 1.8%, var(--admin-layer-2));
|
||||
padding: 11px 12px 12px;
|
||||
}
|
||||
|
||||
.admin-price-tier-panel-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.admin-price-tier-panel-heading > div,
|
||||
.admin-price-tier-cost-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 9px;
|
||||
}
|
||||
|
||||
.admin-price-tier-panel-heading h3,
|
||||
.admin-price-tier-cost-title {
|
||||
margin: 0;
|
||||
color: var(--admin-text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-price-tier-panel-heading p,
|
||||
.admin-price-tier-cost-heading p {
|
||||
margin: 0;
|
||||
color: var(--admin-text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-price-tier-match-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: start;
|
||||
gap: 8px 10px;
|
||||
}
|
||||
|
||||
.admin-price-tier-upstream {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.admin-price-tier-billing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(190px, 0.85fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-price-tier-token-grid {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-panel {
|
||||
min-width: 0;
|
||||
margin-top: 10px;
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--foreground) 2.2%, transparent);
|
||||
padding: 9px 10px 10px;
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-header {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-title {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-badge {
|
||||
border: 1px solid color-mix(in srgb, var(--workspace-accent) 24%, transparent);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--workspace-accent) 8%, transparent);
|
||||
padding: 2px 7px;
|
||||
color: color-mix(in srgb, var(--workspace-accent) 72%, var(--admin-text-primary));
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-token-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.admin-price-tier-cost-switch {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.admin-price-tier-alert {
|
||||
margin: 0 16px 16px;
|
||||
}
|
||||
|
||||
.admin-price-tier-helper {
|
||||
display: flex;
|
||||
max-width: none;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 8px;
|
||||
margin-top: 8px;
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--workspace-accent) 3.5%, transparent);
|
||||
padding: 6px 8px;
|
||||
color: var(--admin-text-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-price-tier-helper strong {
|
||||
color: var(--admin-text-secondary);
|
||||
flex: 0 0 auto;
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.admin-price-tier-helper span {
|
||||
min-width: 240px;
|
||||
flex: 1 1 520px;
|
||||
}
|
||||
|
||||
.admin-price-tier-unit-price .ant-input-number,
|
||||
.admin-price-tier-cost-grid .ant-input-number {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@@ -9366,20 +9574,50 @@
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
gap: 7px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--foreground) 3%, transparent);
|
||||
padding: 3px 6px 3px 8px;
|
||||
}
|
||||
|
||||
.admin-price-tier-toggle span {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--admin-text-muted);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.admin-model-editor-add-tier.ant-btn {
|
||||
min-height: 38px;
|
||||
border-color: color-mix(in srgb, var(--workspace-accent) 28%, var(--admin-card-border));
|
||||
background: color-mix(in srgb, var(--workspace-accent) 4%, transparent);
|
||||
}
|
||||
|
||||
.admin-price-tier-billing-mode .ant-segmented {
|
||||
border: 0;
|
||||
background: var(--admin-layer-2);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.admin-price-tier-mode-control .ant-segmented-item,
|
||||
.admin-price-tier-billing-mode .ant-segmented-item {
|
||||
min-height: 28px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.admin-price-tier-mode-control .ant-segmented-item-label,
|
||||
.admin-price-tier-billing-mode .ant-segmented-item-label {
|
||||
min-height: 28px;
|
||||
padding-inline: 12px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.admin-price-tier-mode-control .ant-segmented-item-selected,
|
||||
.admin-price-tier-billing-mode .ant-segmented-item-selected {
|
||||
background: var(--admin-layer-1);
|
||||
box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 7%, transparent);
|
||||
}
|
||||
|
||||
.admin-model-editor-footer-actions {
|
||||
@@ -9441,14 +9679,19 @@
|
||||
:root:not(.dark) .admin-model-editor-drawer .admin-capability-reference-card,
|
||||
:root:not(.dark) .admin-model-editor-drawer .admin-capability-protocol-card,
|
||||
:root:not(.dark) .admin-price-tier-toggle,
|
||||
:root:not(.dark) .admin-price-tier-mode-control .ant-segmented,
|
||||
:root:not(.dark) .admin-price-tier-billing-mode .ant-segmented {
|
||||
border-color: rgba(17, 17, 17, 0.18);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
:root:not(.dark) .admin-price-tier-card {
|
||||
border-color: rgba(17, 17, 17, 0.18);
|
||||
background: #f7f7f7;
|
||||
border-color: rgba(17, 17, 17, 0.1);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
:root:not(.dark) .admin-price-tier-panel {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
:root:not(.dark) .admin-model-editor-drawer .admin-capability-reference-card,
|
||||
@@ -9492,10 +9735,12 @@
|
||||
box-shadow: 0 6px 20px rgb(17 17 17 / 0.08);
|
||||
}
|
||||
|
||||
:root:not(.dark) .admin-price-tier-mode-control .ant-segmented-item-selected,
|
||||
:root:not(.dark) .admin-price-tier-billing-mode .ant-segmented-item-selected {
|
||||
background: #ececec;
|
||||
box-shadow: inset 0 0 0 1px rgb(17 17 17 / 0.12);
|
||||
box-shadow: 0 1px 2px rgb(17 17 17 / 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.admin-model-editor-modal .ant-modal-container {
|
||||
height: calc(100dvh - 24px);
|
||||
@@ -9513,6 +9758,23 @@
|
||||
.admin-model-editor-modal .admin-capability-protocol-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.admin-price-tier-card-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.admin-price-tier-card-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.admin-price-tier-mode-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 7px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
.admin-price-tier-workspace {
|
||||
padding: 10px;
|
||||
}
|
||||
.admin-price-tier-upstream {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -9520,14 +9782,15 @@
|
||||
.admin-model-protocol-grid,
|
||||
.admin-price-tier-billing-grid,
|
||||
.admin-price-tier-match-grid,
|
||||
.admin-price-tier-token-grid {
|
||||
.admin-price-tier-token-grid,
|
||||
.admin-price-tier-cost-token-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
.admin-model-editor-section,
|
||||
.admin-price-tier-card-body {
|
||||
.admin-price-tier-workspace.is-advanced {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -10840,7 +11103,6 @@ html[data-magicui-theme-vt="active"] .admin-shell * {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
.admin-banner-icon-picker-option.is-active {
|
||||
border-color: color-mix(in srgb, var(--foreground) 22%, transparent);
|
||||
background: var(--control-selected-bg);
|
||||
@@ -10943,3 +11205,37 @@ html[data-magicui-theme-vt="active"] .admin-shell * {
|
||||
.admin-banner-type-card[data-banner-type="warning"] .admin-banner-type-card-preview {
|
||||
background: var(--notice-banner-surface-warning);
|
||||
}
|
||||
|
||||
/* 模型定价编辑器:弹窗内只保留一个滚动区域。 */
|
||||
.admin-model-editor-modal .ant-modal-body {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-model-editor-modal .admin-model-editor-form {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-model-editor-modal .admin-model-editor-form > .ant-tabs {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-model-editor-modal .admin-model-editor-form > .ant-tabs > .ant-tabs-body-holder {
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.admin-model-editor-modal .admin-model-editor-form > .ant-tabs > .ant-tabs-body-holder > .ant-tabs-content,
|
||||
.admin-model-editor-modal .admin-model-editor-form > .ant-tabs > .ant-tabs-body-holder > .ant-tabs-content > .ant-tabs-tabpane {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@@ -83,11 +83,12 @@ describe("pricing write validation", () => {
|
||||
video.priceTiers[0].outputTokenPrice = 0.000001;
|
||||
expect(() => validateChannelModelPrices(video)).not.toThrow();
|
||||
});
|
||||
test("accepts audio-only video price conditions and rejects invalid audio selectors", () => {
|
||||
for (const videoGenerateAudio of ["true", "false"]) {
|
||||
expect(() => validateChannelModelPrices({ ...draft, capability: "video", protocol: "volcengine-ark-video", priceTiers: [{ ...defaultPriceTier("advanced"), billingMode: "token", videoGenerateAudio }] })).not.toThrow();
|
||||
test("rejects removed video conditions and accepts visible operation or resolution matches", () => {
|
||||
for (const hiddenCondition of [{ videoGenerateAudio: "false" }, { videoSeconds: 5 }, { imageCount: 2 }]) {
|
||||
expect(() => validateChannelModelPrices({ ...draft, capability: "video", protocol: "volcengine-ark-video", priceTiers: [{ ...defaultPriceTier("advanced"), billingMode: "token", ...hiddenCondition }] })).toThrow("匹配条件");
|
||||
}
|
||||
expect(() => validateChannelModelPrices({ ...draft, capability: "video", protocol: "volcengine-ark-video", priceTiers: [{ ...defaultPriceTier("advanced"), videoGenerateAudio: "yes" }] })).toThrow("有声或无声");
|
||||
expect(() => validateChannelModelPrices({ ...draft, capability: "video", protocol: "volcengine-ark-video", priceTiers: [{ ...defaultPriceTier("advanced"), operation: "image_to_video" }] })).not.toThrow();
|
||||
expect(() => validateChannelModelPrices({ ...draft, capability: "video", protocol: "volcengine-ark-video", priceTiers: [{ ...defaultPriceTier("advanced"), resolution: "1080p" }] })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -70,21 +70,22 @@ describe("channel model price tier defaults", () => {
|
||||
|
||||
expect(priceTierToForm(base).matchMode).toBe("default");
|
||||
expect(priceTierToForm({ ...base, selector: { quality: "2k" } }).matchMode).toBe("advanced");
|
||||
expect(priceTierToForm({ ...base, selector: { videoGenerateAudio: "false" } })).toMatchObject({ matchMode: "advanced", videoGenerateAudio: "false" });
|
||||
expect(priceTierToForm({ ...base, selector: { videoGenerateAudio: "false" } })).toMatchObject({ matchMode: "default", videoGenerateAudio: "false" });
|
||||
});
|
||||
|
||||
test("preserves explicit silent and audible selectors but omits the wildcard", () => {
|
||||
test("keeps video matching limited to operation and resolution", () => {
|
||||
for (const videoGenerateAudio of ["true", "false"]) {
|
||||
const tier = { ...defaultPriceTier("advanced"), videoGenerateAudio };
|
||||
expect(skuSelectorFromForm("video", tier)).toEqual({ videoGenerateAudio });
|
||||
expect(skuSelectorFromForm("image", tier)).toEqual({});
|
||||
const tier = { ...defaultPriceTier("advanced"), operation: "image_to_video", resolution: "1080p", videoSeconds: 10, videoGenerateAudio, imageCount: 2 };
|
||||
expect(skuSelectorFromForm("video", tier)).toEqual({ operation: "image_to_video", vquality: "1080p" });
|
||||
expect(priceTierVideoSecondsFromForm("video", tier)).toBe(0);
|
||||
expect(skuSelectorFromForm("image", { ...defaultPriceTier("advanced"), videoSeconds: 10, videoGenerateAudio, imageCount: 2 })).toEqual({});
|
||||
}
|
||||
expect(skuSelectorFromForm("video", defaultPriceTier("advanced"))).toEqual({});
|
||||
});
|
||||
|
||||
test("video Token writes discard hidden text prices without changing the configured video rate", () => {
|
||||
const tier = { ...defaultPriceTier("advanced"), billingMode: "token" as const, videoGenerateAudio: "false", inputTokenPrice: 10, outputTokenPrice: 0.25, cachedTokenPrice: 5 };
|
||||
expect(priceTierPayloadFromForm("video", tier, "seedance")).toMatchObject({ selector: { videoGenerateAudio: "false" }, providerModelKey: "seedance", inputTokenPriceMicrocredits: 0, outputTokenPriceMicrocredits: 250_000, cachedTokenPriceMicrocredits: 0 });
|
||||
expect(priceTierPayloadFromForm("video", tier, "seedance")).toMatchObject({ selector: {}, videoSeconds: 0, providerModelKey: "seedance", inputTokenPriceMicrocredits: 0, outputTokenPriceMicrocredits: 250_000, cachedTokenPriceMicrocredits: 0 });
|
||||
expect(priceTierPayloadFromForm("text", tier, "text-model")).toMatchObject({ inputTokenPriceMicrocredits: 10_000_000, outputTokenPriceMicrocredits: 250_000, cachedTokenPriceMicrocredits: 5_000_000 });
|
||||
expect(priceTierPayloadFromForm("video", { ...tier, outputTokenPrice: 0 }, "seedance").outputTokenPriceMicrocredits).toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user