feat(billing): 火山方舟视频计费 - 支持真实 Token 预授权与结算
Refs #228 Co-authored-by: MasterChiefCN <49179444+MasterChiefCN@users.noreply.github.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { ModelIcon } from "@/components/model-picker";
|
||||
import { ModelCapabilityEditor } from "@/components/model-capability-editor";
|
||||
import { CapabilityCardPicker, ProtocolCardPicker, type ModelCapabilityChoice } from "@/components/model-protocol-picker";
|
||||
import { defaultModelCapabilityConfig, type ModelCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { MODEL_PROTOCOLS, modelProtocolCapability, modelProtocolDefinition, modelProtocolLabel, type ModelProtocol } from "@/lib/model-protocols";
|
||||
import { MODEL_PROTOCOLS, modelProtocolCapability, modelProtocolDefinition, modelProtocolLabel, modelProtocolSupportsTokenBilling, type ModelProtocol } from "@/lib/model-protocols";
|
||||
import { createAdminChannelModel, deleteAdminChannelModel, fetchAdminChannelModels, listAdminChannelModels, testAdminChannelModel, updateAdminChannelModel, type ChannelModel } from "@/services/api/wallet";
|
||||
import type { ModelChannel } from "@/stores/use-config-store";
|
||||
import { AdminPageFrame } from "./admin-shell";
|
||||
@@ -46,6 +46,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const billingMode = Form.useWatch("billingMode", form) || "fixed_request";
|
||||
const modelCapability = Form.useWatch("capability", form);
|
||||
const modelProtocol = Form.useWatch("protocol", form);
|
||||
const modelKey = Form.useWatch("modelKey", form) || "";
|
||||
|
||||
const reload = async () => {
|
||||
@@ -164,11 +165,13 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
if (changed.protocol && (modelCapability === "image" || modelCapability === "video")) {
|
||||
form.setFieldValue("capabilityConfig", defaultModelCapabilityConfig(changed.protocol, form.getFieldValue("modelKey")));
|
||||
}
|
||||
if (!changed.capability) return;
|
||||
const currentBillingMode = form.getFieldValue("billingMode") as ChannelModel["billingMode"] | undefined;
|
||||
if ((currentBillingMode === "per_second" && changed.capability !== "video") || (currentBillingMode === "token" && changed.capability !== "text")) {
|
||||
const currentCapability = form.getFieldValue("capability") as EditableCapability | undefined;
|
||||
const currentProtocol = form.getFieldValue("protocol") as ModelProtocol | undefined;
|
||||
if ((currentBillingMode === "per_second" && currentCapability !== "video") || (currentBillingMode === "token" && !modelProtocolSupportsTokenBilling(currentCapability, currentProtocol))) {
|
||||
form.setFieldValue("billingMode", "fixed_request");
|
||||
}
|
||||
if (!changed.capability) return;
|
||||
const current = form.getFieldValue("protocol") as ModelProtocol | undefined;
|
||||
if (modelProtocolCapability(current) !== changed.capability) {
|
||||
const nextProtocol = MODEL_PROTOCOLS.find((item) => item.capability === changed.capability)?.value;
|
||||
@@ -258,20 +261,29 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel:
|
||||
</Form.Item>
|
||||
{modelCapability === "image" || modelCapability === "video" ? <Form.Item name="capabilityConfig" rules={[{ required: true, message: `请配置${modelCapability === "image" ? "图片" : "视频"}能力参数` }]}><ModelCapabilityEditor capability={modelCapability} model={modelKey} protocol={form.getFieldValue("protocol")} /></Form.Item> : null}
|
||||
<Form.Item name="billingMode" label="计费方式" rules={[{ required: true }]}>
|
||||
<Segmented block options={[{ label: "按次计费", value: "fixed_request" }, { label: "按秒计费", value: "per_second", disabled: modelCapability !== "video" }, { label: "Token 计费", value: "token", disabled: modelCapability !== "text" }]} />
|
||||
<Segmented block options={[{ label: "按次计费", value: "fixed_request" }, { label: "按秒计费", value: "per_second", disabled: modelCapability !== "video" }, { label: "Token 计费", value: "token", disabled: !modelProtocolSupportsTokenBilling(modelCapability, modelProtocol) }]} />
|
||||
</Form.Item>
|
||||
{billingMode === "token" ? (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
<Form.Item name="inputTokenPrice" label="输入 / 百万 Token" rules={[{ required: true, message: "请输入输入价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item name="outputTokenPrice" label="输出 / 百万 Token" rules={[{ required: true, message: "请输入输出价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item name="cachedTokenPrice" label="缓存 / 百万 Token" rules={[{ required: true, message: "请输入缓存价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
modelCapability === "video" ? (
|
||||
<div>
|
||||
<Form.Item name="outputTokenPrice" label="视频 / 百万 Token" rules={[{ required: true, message: "请输入视频 Token 价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0.000001} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<div className="mb-4 text-xs text-foreground/45">仅火山方舟视频协议可用;成功后按任务查询响应的 usage.completion_tokens 结算。</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
<Form.Item name="inputTokenPrice" label="输入 / 百万 Token" rules={[{ required: true, message: "请输入输入价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item name="outputTokenPrice" label="输出 / 百万 Token" rules={[{ required: true, message: "请输入输出价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item name="cachedTokenPrice" label="缓存 / 百万 Token" rules={[{ required: true, message: "请输入缓存价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Form.Item name="unitPrice" label={billingMode === "per_second" ? "每秒消耗积分" : "每次消耗积分"} rules={[{ required: true, message: "请输入积分价格" }]}>
|
||||
<InputNumber style={{ width: "100%" }} min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
@@ -303,9 +315,7 @@ function billingSummary(item: ChannelModel) {
|
||||
}
|
||||
return (
|
||||
<div className="text-xs leading-5">
|
||||
<div>输入 {formatCredits(item.inputTokenPriceMicrocredits)} / 百万</div>
|
||||
<div>输出 {formatCredits(item.outputTokenPriceMicrocredits)} / 百万</div>
|
||||
<div>缓存 {formatCredits(item.cachedTokenPriceMicrocredits)} / 百万</div>
|
||||
{item.capability === "video" ? <div>视频 {formatCredits(item.outputTokenPriceMicrocredits)} / 百万</div> : <><div>输入 {formatCredits(item.inputTokenPriceMicrocredits)} / 百万</div><div>输出 {formatCredits(item.outputTokenPriceMicrocredits)} / 百万</div><div>缓存 {formatCredits(item.cachedTokenPriceMicrocredits)} / 百万</div></>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user