Browse Source

积分计算修改

feature/video-erase
Luckyu_js 1 week ago
parent
commit
5450860ced
  1. 6
      src/components/__tests__/GenerationComposer.test.tsx
  2. 6
      src/components/composer/GenerationComposer.tsx
  3. 36
      src/utils/__tests__/composerPricing.test.ts
  4. 53
      src/utils/composerPricing.ts

6
src/components/__tests__/GenerationComposer.test.tsx

@ -1761,7 +1761,7 @@ describe("GenerationComposer", () => {
});
});
it("uses the Seedance estimate endpoint for PopiServer Seedance models", async () => {
it("uses the estimate endpoint for PopiServer billingMethod 2 video models", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith("/api/points/quote")) {
@ -1791,7 +1791,7 @@ describe("GenerationComposer", () => {
aiModelCodeAlias: "seedance-video-pro",
type: 2,
subType: POPI_VIDEO_SUBTYPE_IMAGE_TO_VIDEO,
billingMethod: 1,
billingMethod: 2,
},
},
}),
@ -1864,7 +1864,7 @@ describe("GenerationComposer", () => {
aiModelCodeAlias: "seedance-video-pro",
type: 2,
subType: POPI_VIDEO_SUBTYPE_IMAGE_TO_VIDEO,
billingMethod: 1,
billingMethod: 2,
},
},
}),

6
src/components/composer/GenerationComposer.tsx

@ -39,7 +39,7 @@ import {
} from "@/utils/composerReferenceSubjects";
import {
buildPopiserverPriceQuoteRequest,
getQuotedPointAmount,
resolveQuotedPointAmount,
type PopiserverPriceQuoteResponse,
} from "@/utils/composerPricing";
import {
@ -700,7 +700,9 @@ function ComposerEditor({
signal: controller.signal,
});
const data = response.ok ? await response.json() as PopiserverPriceQuoteResponse : null;
const amount = data ? getQuotedPointAmount(data) : null;
const amount = data
? resolveQuotedPointAmount(data, quoteRequest.source, normalizedConfig.batchSize)
: null;
if (priceRequestIdRef.current === requestId) {
setPriceState({ amount, isLoading: false });
}

36
src/utils/__tests__/composerPricing.test.ts

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
buildPopiserverPriceQuoteRequest,
getQuotedPointAmount,
resolveQuotedPointAmount,
} from "@/utils/composerPricing";
import type { SelectedModel } from "@/types";
import type { GenerationNodeConfig } from "@/utils/generationConfig";
@ -64,13 +65,12 @@ describe("composerPricing", () => {
duration: 8,
input_video_duration_seconds: 12,
},
batchSize: 1,
input_video_duration_seconds: 12,
},
});
});
it("skips task price quote requests for llm and Seedance estimates", () => {
it("skips quote requests for llm and models without a billing method", () => {
expect(buildPopiserverPriceQuoteRequest(context({
capability: "llm",
config: draft(),
@ -80,10 +80,10 @@ describe("composerPricing", () => {
config: draft({
selectedModel: model({ displayName: "Seedance" }),
}),
}))).toMatchObject({ source: "estimate" });
}))).toBeNull();
});
it("uses PopiServer estimate only for billingMethod 2 or Seedance models", () => {
it("routes purely by billingMethod (2 -> estimate, 1 -> task-price)", () => {
expect(buildPopiserverPriceQuoteRequest(context({ config: draft({
selectedModel: model({ billingMethod: 2 }),
}) }))).toMatchObject({ source: "estimate" });
@ -96,34 +96,42 @@ describe("composerPricing", () => {
subType: 101,
}),
}) }))).toMatchObject({ source: "task-price" });
// Seedance name no longer forces estimate; billingMethod 1 -> task-price.
expect(buildPopiserverPriceQuoteRequest(context({
capability: "video",
config: draft({
selectedModel: model({
displayName: "Seedance Pro",
billingMethod: 1,
aiModelId: 44,
}),
}),
}))).toMatchObject({ source: "estimate" });
}))).toMatchObject({ source: "task-price" });
});
it("forwards video count (batchSize) in Seedance estimate payloads", () => {
it("does not forward batchSize in estimate payloads (backend ignores it)", () => {
const request = buildPopiserverPriceQuoteRequest(context({
capability: "video",
config: draft({
selectedModel: model({ displayName: "Seedance Pro", billingMethod: 1 }),
selectedModel: model({ displayName: "Seedance Pro", billingMethod: 2 }),
parameters: { duration: 5 },
batchSize: 3,
}),
}));
expect(request).toMatchObject({
source: "estimate",
payload: {
estimation_type: "auto",
batchSize: 3,
},
});
expect(request?.source).toBe("estimate");
expect(request?.payload).not.toHaveProperty("batchSize");
});
it("scales estimate points by batchSize but leaves task-price untouched", () => {
const response = { data: { points: 20 } };
// estimate is a per-single quote -> multiply by video/image count
expect(resolveQuotedPointAmount(response, "estimate", 3)).toBe(60);
expect(resolveQuotedPointAmount(response, "estimate", undefined)).toBe(20);
expect(resolveQuotedPointAmount(response, "estimate", 0)).toBe(20);
// task-price already accounts for batchSize upstream
expect(resolveQuotedPointAmount(response, "task-price", 3)).toBe(20);
expect(resolveQuotedPointAmount({ data: { points: "bad" } }, "estimate", 3)).toBeNull();
});
it("builds unified PopiServer quote requests", () => {

53
src/utils/composerPricing.ts

@ -48,15 +48,6 @@ function stringFromUnknown(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function isSeedanceVideoEstimateModel(model: SelectedModel): boolean {
return /seedance/i.test([
model.modelId,
model.displayName,
stringFromUnknown(selectedModelMetadataValue(model, "aiModelCode")),
stringFromUnknown(selectedModelMetadataValue(model, "aiModelCodeAlias")),
].filter(Boolean).join(" "));
}
function getPopiserverBillingMethod(model: SelectedModel): number | null {
const billingMethod = finiteNumberFromUnknown(selectedModelMetadataValue(model, "billingMethod"));
return billingMethod === null ? null : Math.trunc(billingMethod);
@ -64,7 +55,7 @@ function getPopiserverBillingMethod(model: SelectedModel): number | null {
function shouldUsePopiserverPointsEstimate(model: SelectedModel): boolean {
if (model.provider !== "popiserver") return false;
return getPopiserverBillingMethod(model) === 2 || isSeedanceVideoEstimateModel(model);
return getPopiserverBillingMethod(model) === 2;
}
function shouldUsePopiserverTaskPrice(model: SelectedModel): boolean {
@ -72,10 +63,6 @@ function shouldUsePopiserverTaskPrice(model: SelectedModel): boolean {
return getPopiserverBillingMethod(model) === 1;
}
function shouldUsePopiserverSeedanceEstimate(model: SelectedModel): boolean {
return model.provider === "popiserver" && isSeedanceVideoEstimateModel(model);
}
function getPopiserverModelName(model: SelectedModel): string {
return (
stringFromUnknown(selectedModelMetadataValue(model, "aiModelCodeAlias")) ||
@ -108,20 +95,6 @@ function buildPopiserverPointsEstimatePayload(
};
}
if (shouldUsePopiserverSeedanceEstimate(selectedModel)) {
return {
model_name: getPopiserverModelName(selectedModel),
estimation_type: "auto",
parameters: {
...(config.parameters ?? {}),
input_video_duration_seconds: inputVideoDurationSeconds,
},
// Seedance pricing scales with the requested video count; forward batchSize
// top-level like the generic estimate branch so switching 视频数量 re-quotes.
...(config.batchSize ? { batchSize: config.batchSize } : {}),
};
}
return {
model_name: getPopiserverModelName(selectedModel),
estimation_type: "auto",
@ -134,7 +107,8 @@ function buildPopiserverPointsEstimatePayload(
...(inputVideoDurationSeconds > 0 ? { input_video_duration_seconds: inputVideoDurationSeconds } : {}),
},
...(Object.keys(config.extraTaskParams ?? {}).length > 0 ? { extraTaskParams: config.extraTaskParams } : {}),
...(config.batchSize ? { batchSize: config.batchSize } : {}),
// The estimate endpoint does not support batchSize; it returns points for a
// single generation. batchSize is applied client-side in resolveQuotedPointAmount().
...(config.audioVoiceId ? { audioVoiceId: config.audioVoiceId } : {}),
...(inputVideoDurationSeconds > 0 ? { input_video_duration_seconds: inputVideoDurationSeconds } : {}),
};
@ -144,6 +118,27 @@ export function getQuotedPointAmount(response: PopiserverPriceQuoteResponse): nu
return finiteNumberFromUnknown(response.data?.points);
}
/**
* Resolves the total point amount to display for a quote.
*
* The estimate endpoint quotes a single generation and ignores batchSize, so we
* multiply by the requested count on the client. task-price already accounts for
* batchSize upstream, so it is returned as-is.
*/
export function resolveQuotedPointAmount(
response: PopiserverPriceQuoteResponse,
source: PopiserverPriceQuoteSource,
batchSize: number | undefined
): number | null {
const points = getQuotedPointAmount(response);
if (points === null) return null;
if (source !== "estimate") return points;
const count = typeof batchSize === "number" && Number.isFinite(batchSize) && batchSize > 0
? Math.trunc(batchSize)
: 1;
return points * count;
}
function buildComposerPopiserverTaskPricePayload(
context: ComposerPricingContext
) {

Loading…
Cancel
Save