fix(model-picker): 模型选择 - 修复候选报价显示与同渠道模型跳回 (#556)

Original-Commit: 7a5e17e2112c3c23e081a1ecf3c33a654cf1e907
fix(model-picker): 模型选择 - 修复候选报价显示与同渠道模型跳回
This commit is contained in:
Rou
2026-09-20 22:44:59 +08:00
committed by ddCat
parent 93bc079b89
commit 19c9a207c9
6 changed files with 119 additions and 12 deletions

View File

@@ -1,11 +1,13 @@
import { expect, test } from "bun:test";
import { groupModelsForPicker, modelChannelLabel } from "../src/lib/model-picker-groups";
import { modelCompatibilityError, resolveCompatibleModel } from "../src/lib/model-selection";
import { groupModelsByDisplayName, modelCompatibilityError, modelGroupReferenceLimits, resolveCompatibleModel } from "../src/lib/model-selection";
import { modelQuoteRequest, priceTiersForCurrentSelection, priceTierSummaryLabel } from "../src/lib/model-pricing";
import { systemChannelModelChannels } from "../src/lib/user-session";
import type { PublicChannelCatalog, PublicChannelModel } from "../src/services/api/logical-models";
import { defaultConfig, normalizeConfigSnapshot, resolveModelRequestConfig, selectableModelsByCapability } from "../src/stores/use-config-store";
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
import { buildGenerationConfig } from "../src/lib/canvas/canvas-project-generation";
import { CanvasNodeType } from "../src/types/canvas";
function model(label = "", price = 300000, modelKey = "seedance-2.0", displayName = modelKey === "seedance-2.0" ? "Seedance 2.0" : modelKey): PublicChannelModel {
return {
@@ -62,6 +64,42 @@ test("selection and quote keep the chosen channel even when another channel is c
expect(resolveModelRequestConfig(config, value)).toMatchObject({ channelId: "b", model: "seedance-2.0" });
});
function sameChannelVariants() {
const channels = systemChannelModelChannels([{ id: "comfy", name: "Comfy", displayName: "Comfy", models: [
model("高速版", 100_000, "h3-fast", "MiniMax H3"),
model("多图一致性", 200_000, "h3-multi", "MiniMax H3"),
] }]);
return normalizeConfigSnapshot({ config: { ...defaultConfig, channels, model: "comfy::h3-multi", videoModel: "comfy::h3-multi" } }).config;
}
test("same-channel system variants retain explicit model identity through selection, generation and quote", () => {
const config = sameChannelVariants();
const options = selectableModelsByCapability(config, "video");
expect(groupModelsForPicker(config, options)[0].models).toHaveLength(2);
expect(groupModelsByDisplayName(config, options).map((group) => group.models)).toEqual(options.map((value) => [value]));
for (const value of options) {
expect(resolveCompatibleModel(config, value, { capability: "video" })).toBe(value);
const generation = buildGenerationConfig(config, {
id: "video", type: CanvasNodeType.Video, title: "Video", position: { x: 0, y: 0 }, width: 100, height: 100,
metadata: { model: value, generationMode: "video" },
}, "video");
expect(generation.model).toBe(value);
expect(resolveModelRequestConfig(generation, generation.model)).toMatchObject({ channelId: "comfy", model: value.split("::")[1] });
expect(modelQuoteRequest(config, value, "video")).toMatchObject({ channelId: "comfy", modelKey: value.split("::")[1] });
}
});
test("same-channel system variants do not borrow capabilities or reroute incompatible selections", () => {
const config = sameChannelVariants();
config.channels[0].modelCosts![0].capabilityConfig!.video!.references.maxImages = 0;
config.channels[0].modelCosts![1].capabilityConfig!.video!.references.maxImages = 2;
const requirements = { capability: "video" as const, input: { textCount: 1, imageCount: 1, videoCount: 0, audioCount: 0, characterCount: 0 } };
expect(modelGroupReferenceLimits(config, "comfy::h3-fast", "video")?.maxImages).toBe(0);
expect(modelCompatibilityError(config, "comfy::h3-fast", requirements)).not.toBe("");
expect(resolveCompatibleModel(config, "comfy::h3-fast", requirements)).toBe("");
expect(resolveCompatibleModel(config, "comfy::h3-multi", requirements)).toBe("comfy::h3-multi");
});
test("different display names in one channel create distinct first-level groups", () => {
const config = fixture();
const extra = systemChannelModelChannels([{ id: "volc", name: "火山引擎", displayName: "火山引擎", models: [

View File

@@ -0,0 +1,58 @@
import { expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { ModelLabel, ModelPicker } from "../src/components/model-picker";
import { canvasThemes } from "../src/lib/canvas-theme";
import { requestCreditCost } from "../src/lib/model-pricing";
import type { ModelRequirements } from "../src/lib/model-selection";
import { createModelChannel, defaultConfig, normalizeConfigSnapshot } from "../src/stores/use-config-store";
function fixture() {
const channel = createModelChannel({
id: "comfy", name: "Comfy", scope: "system", models: ["h3-multi"],
modelCosts: [{
model: "h3-multi", displayName: "MiniMax H3", channelLabel: "多图一致性", capability: "video",
pricePolicy: "channel", billingMode: "per_second", unitPriceMicrocredits: 100_000,
logicalPriceTiers: [480, 720].map((resolution, index) => ({
selector: { vquality: resolution + "p", videoSeconds: "6" }, resolution: resolution + "p", videoSeconds: 6,
billingMode: "per_second", unitPriceMicrocredits: (index + 1) * 100_000,
inputTokenPriceMicrocredits: 0, outputTokenPriceMicrocredits: 0, cachedTokenPriceMicrocredits: 0,
})),
}],
});
return normalizeConfigSnapshot({ config: { ...defaultConfig, channels: [channel], model: "comfy::h3-multi", videoModel: "comfy::h3-multi" } }).config;
}
const requirements: ModelRequirements = { capability: "video", videoSeconds: "5", options: { vquality: "1080p" } };
test("candidate rows show their configured prices instead of applying the previous model's specifications", () => {
const config = fixture();
for (const theme of [canvasThemes.light, canvasThemes.dark]) {
const markup = renderToStaticMarkup(<ModelLabel config={config} model={config.model} capability="video" theme={theme}
creationVariant showConfiguredModelName={false} requirements={requirements} showPrice showDescription />);
expect(markup).toContain("0.1-0.2 积分/秒");
expect(markup).not.toContain("当前规格无报价");
}
});
test("candidate rows do not disguise missing prices as zero", () => {
const config = fixture();
config.channels[0].modelCosts![0].logicalPriceTiers = [];
const markup = renderToStaticMarkup(<ModelLabel config={config} model={config.model} capability="video" theme={canvasThemes.dark}
creationVariant showConfiguredModelName={false} requirements={requirements} showPrice showDescription />);
expect(markup).toContain("未配置");
expect(markup).not.toContain("0 积分");
});
test("the selected model and request estimate still require an exact specification match", () => {
const config = fixture();
const render = (value: ModelRequirements) => renderToStaticMarkup(<ModelPicker config={config} value={config.model} capability="video"
requirements={value} onChange={() => {}} />);
expect(render(requirements)).toContain("当前规格无报价");
const cost = (value: ModelRequirements) => requestCreditCost({ channelMode: "remote", modelCosts: config.channels[0].modelCosts,
model: "h3-multi", capability: "video", config, requirements: value, seconds: "6" });
expect(cost(requirements)).toBeNull();
const matching = { ...requirements, videoSeconds: "6", options: { vquality: "720p" } };
expect(render(matching)).toContain("0.2 积分/秒");
expect(render(matching)).not.toContain("当前规格无报价");
expect(cost(matching)).toBeCloseTo(1.2);
});