fix(models): 系统渠道模型 - 上游 ID 重命名级联价格档并补管理端可见性 (#514)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import { defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type Mode
|
||||
import type { ModelProtocolDefinition } from "@/lib/model-protocols";
|
||||
import { createAdminChannelModel, testAdminChannelModel, updateAdminChannelModel, type ChannelModel } from "@/services/api/wallet";
|
||||
import type { ModelChannel } from "@/stores/use-config-store";
|
||||
import { defaultPriceTier, priceTierResolutionFromForm, priceTierVideoSecondsFromForm, skuSelectorFromForm } from "./channel-model-price-tier-form";
|
||||
import { defaultPriceTier, normalizeUpstreamModelKey, priceTierResolutionFromForm, priceTierVideoSecondsFromForm, skuSelectorFromForm } from "./channel-model-price-tier-form";
|
||||
import { PriceTierFields } from "./channel-model-price-tier-fields";
|
||||
import { changeChannelModelCapability, editorSectionForField, initialChannelModelValues, validateChannelModelPrices, validateChannelModelProtocol, type ChannelModelFormValues as FormValues, type EditorSection } from "./channel-model-editor-form";
|
||||
|
||||
@@ -50,6 +50,8 @@ export function ChannelModelEditor({
|
||||
const modelEnabled = Form.useWatch("enabled", form) !== false;
|
||||
const priceTiers = Form.useWatch("priceTiers", form) || [];
|
||||
const hasDefaultPriceTier = priceTiers.some((tier) => tier.matchMode === "default");
|
||||
const tiersWithOwnUpstream = priceTiers.filter((tier) => tier.providerModelKey?.trim());
|
||||
const modelUpstream = normalizeUpstreamModelKey(providerModelKey || modelKey);
|
||||
const busy = saving || testing;
|
||||
|
||||
const requestClose = () => {
|
||||
@@ -252,7 +254,7 @@ export function ChannelModelEditor({
|
||||
placeholder="例如:seedance-2-5"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="providerModelKey" label="上游模型 ID" tooltip="实际发送给供应商;留空时使用产品模型标识。">
|
||||
<Form.Item name="providerModelKey" label="上游模型 ID" tooltip="实际发送给供应商;留空时使用产品模型标识。价格档可配置独立上游 ID,命中时优先于此处。">
|
||||
<Input placeholder="留空则使用产品模型标识" />
|
||||
</Form.Item>
|
||||
<Form.Item name="displayName" label="后台显示名称" tooltip="仅用于后台识别,不改变调用 ID。">
|
||||
@@ -262,6 +264,14 @@ export function ChannelModelEditor({
|
||||
<ModelIconPicker />
|
||||
</Form.Item>
|
||||
</div>
|
||||
{tiersWithOwnUpstream.length ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
title={`${tiersWithOwnUpstream.length} 个价格档配置了独立上游模型 ID`}
|
||||
description="命中的请求会优先使用价格档的上游 ID;修改上方上游模型 ID 时,只有与旧值相同的档位会自动跟随更新,其余保持不变。"
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
<section className="admin-model-editor-section">
|
||||
<SectionHeading title="能力与协议" description="先选任务类型,再选择对应的调用协议;更换后请核对参数与价格。" />
|
||||
@@ -376,6 +386,7 @@ export function ChannelModelEditor({
|
||||
capability={modelCapability}
|
||||
protocol={modelProtocol}
|
||||
capabilityConfig={capabilityConfig}
|
||||
modelUpstream={modelUpstream}
|
||||
onDirty={() => {
|
||||
dirtyRef.current = true;
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Button, Form, Input, InputNumber, Segmented, Select, Switch, type FormInstance } from "antd";
|
||||
import { Alert, Button, Form, Input, InputNumber, Segmented, Select, Switch, type FormInstance } from "antd";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import type { ModelCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { modelProtocolSupportsTokenBilling, type ModelProtocol } from "@/lib/model-protocols";
|
||||
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";
|
||||
|
||||
export function PriceTierFields({
|
||||
index,
|
||||
@@ -12,6 +13,7 @@ export function PriceTierFields({
|
||||
capability,
|
||||
protocol,
|
||||
capabilityConfig,
|
||||
modelUpstream,
|
||||
onDirty,
|
||||
onRemove,
|
||||
}: {
|
||||
@@ -21,6 +23,7 @@ export function PriceTierFields({
|
||||
capability: EditableCapability | undefined;
|
||||
protocol: ModelProtocol | undefined;
|
||||
capabilityConfig?: ModelCapabilityConfig;
|
||||
modelUpstream: string;
|
||||
onDirty: () => void;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
@@ -28,6 +31,10 @@ export function PriceTierFields({
|
||||
const matchMode = Form.useWatch(["priceTiers", index, "matchMode"], form) || "default";
|
||||
const priceConfigured = Form.useWatch(["priceTiers", index, "priceConfigured"], form) !== false;
|
||||
const tierEnabled = Form.useWatch(["priceTiers", index, "enabled"], form) !== false;
|
||||
const tierUpstream = normalizeUpstreamModelKey(Form.useWatch(["priceTiers", index, "providerModelKey"], form));
|
||||
// 统一价格档不展示上游键输入,但历史数据可能固化了独立键;它与模型级不一致时会
|
||||
// 静默改变实际发往供应商的模型,必须显式提示并允许一键恢复“跟随模型默认”。
|
||||
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 || [] : [];
|
||||
@@ -155,6 +162,25 @@ export function PriceTierFields({
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
{staleTierUpstream ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
title="该统一价格档仍指向独立上游模型 ID"
|
||||
description={`命中此档的请求会优先使用「${staleTierUpstream}」,而不是模型默认上游「${modelUpstream}」。`}
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onDirty();
|
||||
form.setFieldValue(["priceTiers", index, "providerModelKey"], "");
|
||||
}}
|
||||
>
|
||||
清除并跟随模型
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { ChannelModel, ChannelModelPriceTier } from "@/services/api/wallet";
|
||||
|
||||
// Node 原生 TypeScript 测试运行器要求保留扩展名,项目编译器不允许该写法。
|
||||
// @ts-expect-error -- Node 原生 TypeScript 测试运行器需要保留扩展名。
|
||||
import { defaultPriceTier, legacyPriceTierToForm, priceTierToForm } from "./channel-model-price-tier-form.ts";
|
||||
|
||||
test("编辑无价格档的旧模型时不再把顶层上游键固化进统一价格档", () => {
|
||||
const legacy: ChannelModel = {
|
||||
id: "model-1",
|
||||
modelKey: "deepseek-v4.1-flash",
|
||||
providerModelKey: "deepseek-v4.1-flash",
|
||||
billingMode: "fixed_request",
|
||||
unitPriceMicrocredits: 10,
|
||||
priceConfigured: true,
|
||||
enabled: true,
|
||||
} as unknown as ChannelModel;
|
||||
|
||||
const tier = legacyPriceTierToForm(legacy);
|
||||
|
||||
assert.equal(tier.providerModelKey, "");
|
||||
assert.equal(tier.matchMode, "default");
|
||||
});
|
||||
|
||||
test("已存档位键在回显时保持原值,清除与否交给管理端告警处理", () => {
|
||||
const tier = priceTierToForm({
|
||||
selector: {},
|
||||
resolution: "*",
|
||||
videoSeconds: 0,
|
||||
providerModelKey: "sku-preview",
|
||||
billingMode: "fixed_request",
|
||||
unitPriceMicrocredits: 10,
|
||||
inputTokenPriceMicrocredits: 0,
|
||||
outputTokenPriceMicrocredits: 0,
|
||||
cachedTokenPriceMicrocredits: 0,
|
||||
priceConfigured: true,
|
||||
enabled: true,
|
||||
} as unknown as ChannelModelPriceTier);
|
||||
|
||||
assert.equal(tier.providerModelKey, "sku-preview");
|
||||
assert.equal(tier.matchMode, "default");
|
||||
});
|
||||
|
||||
test("新建默认价格档不携带上游键", () => {
|
||||
assert.equal(defaultPriceTier().providerModelKey, "");
|
||||
assert.equal(defaultPriceTier("advanced").providerModelKey, "");
|
||||
});
|
||||
@@ -3,6 +3,11 @@ import type { ChannelModel, ChannelModelPriceTier } from "@/services/api/wallet"
|
||||
|
||||
export type PriceTierMatchMode = "default" | "advanced";
|
||||
|
||||
// 与后端保存口径一致:上游键比较前剥掉一次 models/ 前缀,避免极旧存量行误报差异。
|
||||
export function normalizeUpstreamModelKey(value: unknown): string {
|
||||
return String(value || "").trim().replace(/^models\//, "");
|
||||
}
|
||||
|
||||
export type PriceTierFormValues = {
|
||||
matchMode: PriceTierMatchMode;
|
||||
operation: string;
|
||||
@@ -66,7 +71,8 @@ export function priceTierToForm(tier: ChannelModelPriceTier): PriceTierFormValue
|
||||
export function legacyPriceTierToForm(item: ChannelModel): PriceTierFormValues {
|
||||
return {
|
||||
...defaultPriceTier(),
|
||||
providerModelKey: item.providerModelKey || "",
|
||||
// 不预填顶层上游键:统一价格档保持“跟随模型默认上游 ID”,
|
||||
// 否则首次保存会把当时的上游键固化进档位,之后模型级重命名会被旧值遮蔽。
|
||||
billingMode: item.billingMode,
|
||||
unitPrice: item.unitPriceMicrocredits / 1_000_000,
|
||||
inputTokenPrice: item.inputTokenPriceMicrocredits / 1_000_000,
|
||||
|
||||
Reference in New Issue
Block a user