feat(models): 模型目录与计费 - 按展示名分组并隔离管理员积分成本
This commit is contained in:
@@ -103,7 +103,8 @@ function LogDetail({ log, querying, onQueryProviderTask }: { log: ApiCallLog; qu
|
||||
<span className="text-foreground/35">未返回</span>
|
||||
),
|
||||
],
|
||||
["积分计费", billingText(log)],
|
||||
["销售价格(积分)", billingText(log)],
|
||||
["成本价格(积分)", log.creditCostMicrocredits !== undefined ? `${formatCredits(log.creditCostMicrocredits)} 积分` : log.creditCostConfigured ? "待核算" : "未配置"],
|
||||
["上游成本", log.costAvailable ? <span className="font-mono tabular-nums">{log.currency || "USD"} {(log.estimatedCostMicros / 1_000_000).toFixed(6)}</span> : <span className="text-foreground/35">未配置成本</span>],
|
||||
[
|
||||
"错误信息",
|
||||
|
||||
@@ -96,5 +96,9 @@ export function validateChannelModelPrices(values: Pick<ChannelModelFormValues,
|
||||
}
|
||||
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 之间的有效数值");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,10 +247,10 @@ export function ChannelModelEditor({
|
||||
<Form.Item name="providerModelKey" label="上游模型 ID" tooltip="实际发送给供应商;留空时使用产品模型标识。价格档可配置独立上游 ID,命中时优先于此处。">
|
||||
<Input placeholder="留空则使用产品模型标识" />
|
||||
</Form.Item>
|
||||
<Form.Item name="displayName" label="模型展示名" tooltip="创作端一级菜单名称。同一产品模型标识请使用相同名称和 Logo,不改变调用 ID。">
|
||||
<Form.Item name="displayName" label="模型展示名(一级目录)" tooltip="跨所有系统渠道按此名称分组,例如 MiniMax H3。同名模型归入同一组,不改变调用 ID。">
|
||||
<Input placeholder="不填则使用模型标识" />
|
||||
</Form.Item>
|
||||
<Form.Item name="channelLabel" label="渠道展示名" tooltip="创作端二级菜单名称,仅作用于此渠道中的当前模型。留空使用渠道公开名称。" rules={[{ max: 80, message: "渠道展示名不能超过 80 字" }]}>
|
||||
<Form.Item name="channelLabel" label="渠道展示名(二级目录)" tooltip="此模型下的渠道选项,例如秘塔(满血渠道)。留空使用所属渠道名称。" rules={[{ max: 80, message: "渠道展示名不能超过 80 字" }]}>
|
||||
<Input maxLength={80} placeholder="例如:正常渠道、优惠渠道-993、特惠渠道-730" />
|
||||
</Form.Item>
|
||||
<Form.Item name="icon" label="模型 Logo">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { modelProtocolSupportsTokenBilling, type ModelProtocol } from "@/lib/mod
|
||||
import type { ModelCapabilityChoice as EditableCapability } from "@/components/model-protocol-picker";
|
||||
import type { ChannelModelFormValues as FormValues } from "./channel-model-editor-form";
|
||||
import { normalizeUpstreamModelKey } from "./channel-model-price-tier-form";
|
||||
import { CreditCostFields } from "./credit-cost-fields";
|
||||
|
||||
export function PriceTierFields({
|
||||
index,
|
||||
@@ -84,6 +85,7 @@ export function PriceTierFields({
|
||||
</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
|
||||
@@ -125,6 +127,7 @@ export function PriceTierFields({
|
||||
</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: "请选择生成方式" }]}>
|
||||
|
||||
@@ -23,6 +23,11 @@ export type PriceTierFormValues = {
|
||||
inputTokenPrice: number;
|
||||
outputTokenPrice: number;
|
||||
cachedTokenPrice: number;
|
||||
costConfigured: boolean;
|
||||
costUnitPrice: number;
|
||||
costInputTokenPrice: number;
|
||||
costOutputTokenPrice: number;
|
||||
costCachedTokenPrice: number;
|
||||
priceConfigured: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
@@ -43,6 +48,11 @@ export function defaultPriceTier(matchMode: PriceTierMatchMode = "default"): Pri
|
||||
inputTokenPrice: 0,
|
||||
outputTokenPrice: 0,
|
||||
cachedTokenPrice: 0,
|
||||
costConfigured: false,
|
||||
costUnitPrice: 0,
|
||||
costInputTokenPrice: 0,
|
||||
costOutputTokenPrice: 0,
|
||||
costCachedTokenPrice: 0,
|
||||
priceConfigured: true,
|
||||
enabled: true,
|
||||
};
|
||||
@@ -66,6 +76,11 @@ export function priceTierToForm(tier: ChannelModelPriceTier): PriceTierFormValue
|
||||
inputTokenPrice: tier.inputTokenPriceMicrocredits / 1_000_000,
|
||||
outputTokenPrice: tier.outputTokenPriceMicrocredits / 1_000_000,
|
||||
cachedTokenPrice: tier.cachedTokenPriceMicrocredits / 1_000_000,
|
||||
costConfigured: tier.costPricing?.configured === true,
|
||||
costUnitPrice: (tier.costPricing?.unitPriceMicrocredits ?? 0) / 1_000_000,
|
||||
costInputTokenPrice: (tier.costPricing?.inputTokenPriceMicrocredits ?? 0) / 1_000_000,
|
||||
costOutputTokenPrice: (tier.costPricing?.outputTokenPriceMicrocredits ?? 0) / 1_000_000,
|
||||
costCachedTokenPrice: (tier.costPricing?.cachedTokenPriceMicrocredits ?? 0) / 1_000_000,
|
||||
priceConfigured: tier.priceConfigured,
|
||||
enabled: tier.enabled,
|
||||
};
|
||||
@@ -123,6 +138,13 @@ export function priceTierPayloadFromForm(capability: ModelCapabilityChoice, tier
|
||||
inputTokenPriceMicrocredits: videoTokens ? 0 : Math.round((tier.inputTokenPrice || 0) * 1_000_000),
|
||||
outputTokenPriceMicrocredits: Math.round((tier.outputTokenPrice || 0) * 1_000_000),
|
||||
cachedTokenPriceMicrocredits: videoTokens ? 0 : Math.round((tier.cachedTokenPrice || 0) * 1_000_000),
|
||||
costPricing: {
|
||||
configured: tier.costConfigured,
|
||||
unitPriceMicrocredits: Math.round(tier.costUnitPrice * 1_000_000),
|
||||
inputTokenPriceMicrocredits: videoTokens ? 0 : Math.round(tier.costInputTokenPrice * 1_000_000),
|
||||
outputTokenPriceMicrocredits: Math.round(tier.costOutputTokenPrice * 1_000_000),
|
||||
cachedTokenPriceMicrocredits: videoTokens ? 0 : Math.round(tier.costCachedTokenPrice * 1_000_000),
|
||||
},
|
||||
priceConfigured: tier.priceConfigured !== false,
|
||||
enabled: tier.enabled !== false,
|
||||
};
|
||||
|
||||
21
web/src/pages/admin/components/credit-cost-fields.tsx
Normal file
21
web/src/pages/admin/components/credit-cost-fields.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Form, InputNumber, Switch, type FormInstance } from "antd";
|
||||
import type { ChannelModelFormValues } from "./channel-model-editor-form";
|
||||
|
||||
export function CreditCostFields({ index, form, billingMode, isVideo }: { index: number; form: FormInstance<ChannelModelFormValues>; billingMode: string; isVideo: boolean }) {
|
||||
const configured = Form.useWatch(["priceTiers", index, "costConfigured"], form) === true;
|
||||
const fields = billingMode === "token"
|
||||
? isVideo
|
||||
? [["costOutputTokenPrice", "积分 / 百万视频 Token"]]
|
||||
: [["costInputTokenPrice", "输入 / 百万 Token"], ["costOutputTokenPrice", "输出 / 百万 Token"], ["costCachedTokenPrice", "缓存 / 百万 Token"]]
|
||||
: [["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>
|
||||
{configured ? <div className={billingMode === "token" && !isVideo ? "admin-price-tier-token-grid" : "admin-price-tier-billing-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} />
|
||||
</Form.Item>)}
|
||||
</div> : null}
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user