fix(web): 模型价格 - 修复零积分显示为未配置
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"pretest": "bun test test/admin-ui-regressions.test.ts test/password-reset.test.ts test/oss-settings.test.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:canvas": "bun test test/canvas-media-performance.test.ts test/canvas-node-title-interaction.test.ts test/canvas-spatial-index.test.ts",
|
||||
"test": "bun test test/api-request.test.ts test/video-api-contracts.test.ts test/image-api-contracts.test.ts test/channel-model-catalog.test.ts test/model-protocols.test.ts test/model-capabilities.test.ts test/agent-prompt-cache.test.ts test/canvas-agent-context.test.ts test/canvas-batch-connection.test.ts test/canvas-character-reference.test.ts test/canvas-document.test.ts test/canvas-drawing-engine.test.ts test/canvas-emotion-edit-plan.test.ts test/canvas-extension-nodes.test.ts test/canvas-folder.test.ts test/canvas-generation-layout.test.ts test/canvas-generation-submission.test.ts test/canvas-image-batch-retry.test.ts test/canvas-keyboard.test.ts test/canvas-node-generation-mentions.test.ts test/canvas-node-search.test.ts test/canvas-leafer-viewport.test.ts test/canvas-media-download.test.ts test/canvas-local-runtime.test.ts test/canvas-model-policy.test.ts test/canvas-resource-mention-editor.test.ts test/canvas-resource-references.test.ts test/canvas-storyboard-assets.test.ts test/canvas-storyboard-references.test.ts test/canvas-node-config-reset.test.ts test/canvas-node-registry.test.ts test/canvas-video-frame.test.ts test/canvas-video-regeneration.test.ts test/canvas-video-timeline-segments.test.ts test/create-canvas-handoff.test.ts test/create-library-button.test.ts test/creation-references.test.ts test/director-transform-semantics.test.ts test/director-bone-precedence.test.ts test/director-gesture-transaction.test.ts test/director-transaction-lifecycle.test.ts test/director-session-authority.test.ts test/director-placement.test.ts test/director-resources.test.ts test/director-recovery.test.ts test/director-node-preview.test.ts test/generation-asset-repository.node.test.ts test/generation-storage-consistency.test.ts test/generation-task-durability.node.test.ts test/generation-task-local.test.ts test/generation-task-materializer.test.ts test/task-media-fallback.test.ts test/user-scoped-generation-persistence.test.ts test/local-channel-permission.test.ts test/local-channel-runtime-projection.node.test.mjs test/local-channel-task-center.node.test.mjs test/local-channel-ui.node.test.mjs test/local-cli-settings.test.ts test/local-dreamina-cli.test.ts test/local-dreamina-generation.test.ts test/local-dreamina-model-catalog.test.ts test/local-runtime-bootstrap.test.ts test/local-runtime-connection.test.ts test/local-runtime.test.ts test/client-id.test.ts test/grok-image-prompt-limit.test.ts test/image-resolution-tiers.test.ts test/gemini-image.test.ts test/libtv-import.test.ts test/timeline-to-ffmpeg.test.ts test/canvas-video-segment-args.test.ts test/tapnow-import.test.ts test/prompt-optimizer-drawer.test.ts test/director-save-coordinator.test.ts test/director-save-wiring.test.ts test/director-diagnostics.test.ts test/director-repro-runtime.test.ts test/director-diagnostics-recorder.test.ts test/director-keyframe-editing.test.ts test/director-shortcuts.test.ts test/director-keyframe-delete-wiring.test.ts test/director-templates.test.ts test/director-modes.test.ts test/director-template-mode-wiring.test.ts test/director-view-modes.test.ts && bun test test/local-dreamina-cross-runtime.test.ts",
|
||||
"test": "bun test test/api-request.test.ts test/video-api-contracts.test.ts test/image-api-contracts.test.ts test/channel-model-catalog.test.ts test/model-pricing.test.ts test/model-protocols.test.ts test/model-capabilities.test.ts test/agent-prompt-cache.test.ts test/canvas-agent-context.test.ts test/canvas-batch-connection.test.ts test/canvas-character-reference.test.ts test/canvas-document.test.ts test/canvas-drawing-engine.test.ts test/canvas-emotion-edit-plan.test.ts test/canvas-extension-nodes.test.ts test/canvas-folder.test.ts test/canvas-generation-layout.test.ts test/canvas-generation-submission.test.ts test/canvas-image-batch-retry.test.ts test/canvas-keyboard.test.ts test/canvas-node-generation-mentions.test.ts test/canvas-node-search.test.ts test/canvas-leafer-viewport.test.ts test/canvas-media-download.test.ts test/canvas-local-runtime.test.ts test/canvas-model-policy.test.ts test/canvas-resource-mention-editor.test.ts test/canvas-resource-references.test.ts test/canvas-storyboard-assets.test.ts test/canvas-storyboard-references.test.ts test/canvas-node-config-reset.test.ts test/canvas-node-registry.test.ts test/canvas-video-frame.test.ts test/canvas-video-regeneration.test.ts test/canvas-video-timeline-segments.test.ts test/create-canvas-handoff.test.ts test/create-library-button.test.ts test/creation-references.test.ts test/director-transform-semantics.test.ts test/director-bone-precedence.test.ts test/director-gesture-transaction.test.ts test/director-transaction-lifecycle.test.ts test/director-session-authority.test.ts test/director-placement.test.ts test/director-resources.test.ts test/director-recovery.test.ts test/director-node-preview.test.ts test/generation-asset-repository.node.test.ts test/generation-storage-consistency.test.ts test/generation-task-durability.node.test.ts test/generation-task-local.test.ts test/generation-task-materializer.test.ts test/task-media-fallback.test.ts test/user-scoped-generation-persistence.test.ts test/local-channel-permission.test.ts test/local-channel-runtime-projection.node.test.mjs test/local-channel-task-center.node.test.mjs test/local-channel-ui.node.test.mjs test/local-cli-settings.test.ts test/local-dreamina-cli.test.ts test/local-dreamina-generation.test.ts test/local-dreamina-model-catalog.test.ts test/local-runtime-bootstrap.test.ts test/local-runtime-connection.test.ts test/local-runtime.test.ts test/client-id.test.ts test/grok-image-prompt-limit.test.ts test/image-resolution-tiers.test.ts test/gemini-image.test.ts test/libtv-import.test.ts test/timeline-to-ffmpeg.test.ts test/canvas-video-segment-args.test.ts test/tapnow-import.test.ts test/prompt-optimizer-drawer.test.ts test/director-save-coordinator.test.ts test/director-save-wiring.test.ts test/director-diagnostics.test.ts test/director-repro-runtime.test.ts test/director-diagnostics-recorder.test.ts test/director-keyframe-editing.test.ts test/director-shortcuts.test.ts test/director-keyframe-delete-wiring.test.ts test/director-templates.test.ts test/director-modes.test.ts test/director-template-mode-wiring.test.ts test/director-view-modes.test.ts && bun test test/local-dreamina-cross-runtime.test.ts",
|
||||
"test:director:e2e": "bun scripts/director-p0-chrome-e2e.mjs",
|
||||
"start": "vite preview --host 0.0.0.0 --port 3000",
|
||||
"format": "prettier --write .",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Popover } from "antd";
|
||||
|
||||
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import { modelCapabilityConfigFor, videoDurationOptions } from "@/lib/model-capabilities";
|
||||
import { modelQuoteRequest, normalizeTierResolution, priceTiersForCurrentSelection } from "@/lib/model-pricing";
|
||||
import { formatPriceRange, modelQuoteRequest, normalizeTierResolution, priceTierSummaryLabel, priceTiersForCurrentSelection } from "@/lib/model-pricing";
|
||||
import { compatibleModelInGroup, configuredModelDisplayName, groupModelsByDisplayName, modelCompatibilityError, resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { modelDisplayName, modelIcon, modelOptionName, PUBLIC_MODEL_CATALOG_ID, resolveModelChannel, selectableModelsByCapability, type AiConfig, type ModelCapability } from "@/stores/use-config-store";
|
||||
@@ -400,22 +400,7 @@ function channelTierPriceSummary(
|
||||
visibleTiers: NonNullable<NonNullable<AiConfig["channels"][number]["modelCosts"]>[number]["logicalPriceTiers"]>,
|
||||
allTiers: NonNullable<NonNullable<AiConfig["channels"][number]["modelCosts"]>[number]["logicalPriceTiers"]>,
|
||||
): Extract<ModelMenuPrice, { kind: "tiers" }> {
|
||||
const fixedRequestValues = visibleTiers
|
||||
.filter((tier) => tier.billingMode === "fixed_request")
|
||||
.map((tier) => tier.unitPriceMicrocredits / 1_000_000)
|
||||
.filter((value) => value > 0);
|
||||
const perSecondValues = visibleTiers
|
||||
.filter((tier) => tier.billingMode === "per_second")
|
||||
.map((tier) => tier.unitPriceMicrocredits / 1_000_000)
|
||||
.filter((value) => value > 0);
|
||||
const hasTokenTier = visibleTiers.some((tier) => tier.billingMode === "token");
|
||||
const label = fixedRequestValues.length
|
||||
? formatPriceRange(fixedRequestValues, "积分")
|
||||
: perSecondValues.length
|
||||
? formatPriceRange(perSecondValues, "积分/秒")
|
||||
: hasTokenTier
|
||||
? "按量预估"
|
||||
: "未配置";
|
||||
const label = priceTierSummaryLabel(visibleTiers);
|
||||
return {
|
||||
kind: "tiers",
|
||||
label,
|
||||
@@ -424,12 +409,6 @@ function channelTierPriceSummary(
|
||||
};
|
||||
}
|
||||
|
||||
function formatPriceRange(values: number[], suffix: string) {
|
||||
const unique = Array.from(new Set(values)).sort((left, right) => left - right);
|
||||
const format = (value: number) => value.toLocaleString("zh-CN", { maximumFractionDigits: 3 });
|
||||
return unique.length === 1 ? `${format(unique[0])} ${suffix}` : `${format(unique[0])}-${format(unique[unique.length - 1])} ${suffix}`;
|
||||
}
|
||||
|
||||
function tierResolutionLabel(value: string) {
|
||||
const normalized = normalizeTierResolution(value);
|
||||
return normalized === "*" ? "全部分辨率" : normalized.toUpperCase();
|
||||
|
||||
@@ -51,6 +51,25 @@ export function priceTiersForCurrentSelection(tiers: ModelPriceTier[], capabilit
|
||||
return matched;
|
||||
}
|
||||
|
||||
export function priceTierSummaryLabel(tiers: ModelPriceTier[]) {
|
||||
const unitPrices = (billingMode: ModelPriceTier["billingMode"]) =>
|
||||
tiers
|
||||
.filter((tier) => tier.billingMode === billingMode)
|
||||
.map((tier) => tier.unitPriceMicrocredits / 1_000_000)
|
||||
.filter((value) => Number.isFinite(value) && value >= 0);
|
||||
const fixedRequestValues = unitPrices("fixed_request");
|
||||
const perSecondValues = unitPrices("per_second");
|
||||
if (fixedRequestValues.length) return formatPriceRange(fixedRequestValues, "积分");
|
||||
if (perSecondValues.length) return formatPriceRange(perSecondValues, "积分/秒");
|
||||
return tiers.some((tier) => tier.billingMode === "token") ? "按量预估" : "未配置";
|
||||
}
|
||||
|
||||
export function formatPriceRange(values: number[], suffix: string) {
|
||||
const unique = Array.from(new Set(values)).sort((left, right) => left - right);
|
||||
const format = (value: number) => value.toLocaleString("zh-CN", { maximumFractionDigits: 3 });
|
||||
return unique.length === 1 ? `${format(unique[0])} ${suffix}` : `${format(unique[0])}-${format(unique[unique.length - 1])} ${suffix}`;
|
||||
}
|
||||
|
||||
export function modelQuoteRequest(config: AiConfig, value: string, capability?: ModelCapability, requirements?: ModelRequirements): { logicalModelID: string; intent: ModelRequestIntent } | undefined {
|
||||
if (!capability || !value) return undefined;
|
||||
const channel = resolveModelChannel(config, value);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { defaultConfig } from "../src/stores/use-config-store";
|
||||
import { createModelChannel, defaultConfig } from "../src/stores/use-config-store";
|
||||
import { runBackendGenerationTask, runBackendGenerationTaskBatch } from "../src/services/api/generation-task";
|
||||
import { deleteGenerationTask, formatTaskLog, listGenerationTasks, projectBackendSafeTaskLog, splitGenerationTaskObservationIds, type GenerationTask } from "../src/services/api/task-center";
|
||||
import { isLocalDreaminaBackgroundTask, localDreaminaCancellationCopy, localDreaminaDetachOutcome, projectLocalDreaminaTask } from "../src/services/local-dreamina-task-projection";
|
||||
@@ -24,6 +24,28 @@ function sourceSection(source: string, startMarker: string, endMarker: string) {
|
||||
return compactSource(source.slice(start, end));
|
||||
}
|
||||
|
||||
function backendModelConfig(model: string) {
|
||||
const channel = createModelChannel({
|
||||
id: "system-test-channel",
|
||||
name: "系统测试渠道",
|
||||
baseUrl: "/api/system-test-channel",
|
||||
apiKey: "system",
|
||||
apiFormat: "openai",
|
||||
scope: "system",
|
||||
models: [model],
|
||||
});
|
||||
const selectedModel = `${channel.id}::${model}`;
|
||||
return {
|
||||
...defaultConfig,
|
||||
channelMode: "remote" as const,
|
||||
channels: [channel],
|
||||
model: selectedModel,
|
||||
imageModel: selectedModel,
|
||||
videoModel: selectedModel,
|
||||
audioModel: selectedModel,
|
||||
};
|
||||
}
|
||||
|
||||
test("Dreamina submit failure categories have bounded user-facing messages", () => {
|
||||
const cases = [
|
||||
["dreamina_submit_spawn_failed", "无法启动官方即梦 CLI,任务尚未提交。"],
|
||||
@@ -1286,7 +1308,7 @@ test("remote provider keeps Create resolution semantics and still creates one Ba
|
||||
{
|
||||
mode: "video",
|
||||
prompt: "A remote test clip",
|
||||
config: { ...defaultConfig, model: "default::grok-imagine-video", vquality: "720", quality: "auto" },
|
||||
config: { ...backendModelConfig("grok-imagine-video"), vquality: "720", quality: "auto" },
|
||||
},
|
||||
{
|
||||
createTask: async (input) => {
|
||||
@@ -1354,7 +1376,7 @@ test("remote image video and audio references keep Backend parity without Dreami
|
||||
{
|
||||
mode: item.mode,
|
||||
prompt: "Remote parity fixture",
|
||||
config: { ...defaultConfig, model: "default::provider-neutral-model" },
|
||||
config: backendModelConfig("provider-neutral-model"),
|
||||
...item.references,
|
||||
},
|
||||
{
|
||||
@@ -1673,7 +1695,7 @@ test("Create audio upload converts, previews, removes, and submits through the s
|
||||
projectId: running.projectId,
|
||||
mode: "video",
|
||||
prompt: running.prompt,
|
||||
config: { ...defaultConfig, model: "remote-video-audio-fixture", videoModel: "remote-video-audio-fixture" },
|
||||
config: backendModelConfig("remote-video-audio-fixture"),
|
||||
...references,
|
||||
} as Parameters<typeof runBackendGenerationTask>[0],
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { modelQuoteRequest, normalizeTierResolution, priceTiersForCurrentSelection, requestCreditCost } from "../src/lib/model-pricing";
|
||||
import { modelQuoteRequest, normalizeTierResolution, priceTierSummaryLabel, priceTiersForCurrentSelection, requestCreditCost } from "../src/lib/model-pricing";
|
||||
import type { ModelRequirements } from "../src/lib/model-selection";
|
||||
import { createModelChannel, defaultConfig, normalizeConfigSnapshot, resolveModelChannel, type AiConfig } from "../src/stores/use-config-store";
|
||||
|
||||
@@ -86,6 +86,19 @@ describe("model request pricing", () => {
|
||||
).toBe(0.125);
|
||||
});
|
||||
|
||||
test("displays zero-price system tiers as configured free pricing", () => {
|
||||
const fixedConfig = systemConfig({
|
||||
capability: "image",
|
||||
tiers: [{ selector: {}, billingMode: "fixed_request", unitPriceMicrocredits: 0 }],
|
||||
});
|
||||
const perSecondConfig = systemConfig({
|
||||
tiers: [{ selector: {}, billingMode: "per_second", unitPriceMicrocredits: 0 }],
|
||||
});
|
||||
|
||||
expect(priceTierSummaryLabel(resolveModelChannel(fixedConfig, fixedConfig.model).modelCosts![0]!.logicalPriceTiers!)).toBe("0 积分");
|
||||
expect(priceTierSummaryLabel(resolveModelChannel(perSecondConfig, perSecondConfig.model).modelCosts![0]!.logicalPriceTiers!)).toBe("0 积分/秒");
|
||||
});
|
||||
|
||||
test("uses reference count and operation to avoid a text-video price tier", () => {
|
||||
const config = systemConfig({
|
||||
tiers: [
|
||||
|
||||
Reference in New Issue
Block a user