feat(*): 模型生成链路 - 完善 Agnes 结果兼容、价格展示与请求日志追踪
This commit is contained in:
@@ -133,6 +133,14 @@ describe("Agnes Video 2.5 request contract", () => {
|
||||
expect(state).toEqual({ status: "completed", result: { url: "https://cdn.example.com/result.mp4", mimeType: "video/mp4" } });
|
||||
});
|
||||
|
||||
test("accepts the top-level result url returned by Agnes Video V2.0", async () => {
|
||||
const config = resolveModelRequestConfig(configForAgnes("agnes-video-v2.0"), "agnes::agnes-video-v2.0");
|
||||
const { deps } = providerDeps(config, { polled: { id: "video-1", status: "completed", url: "https://cdn.example.com/v20-result.mp4" } });
|
||||
const state = await pollAgnesVideoTask(deps, config, { id: "video-1", provider: "agnes", model: "agnes::agnes-video-v2.0" });
|
||||
|
||||
expect(state).toEqual({ status: "completed", result: { url: "https://cdn.example.com/v20-result.mp4", mimeType: "video/mp4" } });
|
||||
});
|
||||
|
||||
test("keeps system-channel polls inside the proxy while preserving the root path", () => {
|
||||
const config = { ...resolveModelRequestConfig(configForAgnes(), "agnes::agnes-video-2.5"), baseUrl: "/api/system-agnes" };
|
||||
expect(agnesPollUrl(config, "video/1", "agnes-video-2.5")).toBe("/api/system-agnes/agnesapi?video_id=video%2F1&model_name=agnes-video-2.5");
|
||||
|
||||
132
web/test/model-pricing.test.ts
Normal file
132
web/test/model-pricing.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { modelQuoteRequest, 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";
|
||||
|
||||
function systemConfig(input: { capability?: "image" | "video"; logicalModelId?: string; tiers: Array<{ selector: Record<string, string>; billingMode: "fixed_request" | "per_second" | "token"; unitPriceMicrocredits: number }> }) {
|
||||
const capability = input.capability || "video";
|
||||
const model = capability === "video" ? "agnes-video-2.5" : "image-model";
|
||||
const channel = createModelChannel({
|
||||
id: "system-channel",
|
||||
name: "系统渠道",
|
||||
scope: "system",
|
||||
baseUrl: "/api/system-channel",
|
||||
apiKey: "system",
|
||||
apiFormat: "openai",
|
||||
models: [model],
|
||||
modelCosts: [{
|
||||
model,
|
||||
capability,
|
||||
pricePolicy: "channel",
|
||||
billingMode: input.tiers[0]?.billingMode || "fixed_request",
|
||||
unitPriceMicrocredits: input.tiers[0]?.unitPriceMicrocredits || 0,
|
||||
logicalModelId: input.logicalModelId,
|
||||
logicalPriceTiers: input.tiers.map((tier) => ({
|
||||
...tier,
|
||||
resolution: tier.selector.vquality || "*",
|
||||
videoSeconds: Number(tier.selector.videoSeconds || 0),
|
||||
inputTokenPriceMicrocredits: 0,
|
||||
outputTokenPriceMicrocredits: 0,
|
||||
cachedTokenPriceMicrocredits: 0,
|
||||
})),
|
||||
}],
|
||||
});
|
||||
return normalizeConfigSnapshot({
|
||||
config: {
|
||||
...defaultConfig,
|
||||
channels: [channel],
|
||||
model: `${channel.id}::${model}`,
|
||||
imageModel: capability === "image" ? `${channel.id}::${model}` : defaultConfig.imageModel,
|
||||
videoModel: capability === "video" ? `${channel.id}::${model}` : defaultConfig.videoModel,
|
||||
quality: "high",
|
||||
size: "16:9",
|
||||
vquality: "720P",
|
||||
videoSeconds: "5",
|
||||
count: "3",
|
||||
},
|
||||
}).config;
|
||||
}
|
||||
|
||||
const textVideoRequirements: ModelRequirements = {
|
||||
capability: "video",
|
||||
input: { textCount: 1, imageCount: 0, videoCount: 0, audioCount: 0, characterCount: 0 },
|
||||
videoSeconds: "5",
|
||||
options: { size: "16:9", vquality: "720", videoSeconds: 5 },
|
||||
};
|
||||
|
||||
describe("model request pricing", () => {
|
||||
test("matches the current Agnes resolution and duration tier and totals per-second credits", () => {
|
||||
const config = systemConfig({
|
||||
tiers: [
|
||||
{ selector: { operation: "text_to_video", vquality: "720p", videoSeconds: "5" }, billingMode: "per_second", unitPriceMicrocredits: 25_000 },
|
||||
{ selector: { operation: "text_to_video", vquality: "960p", videoSeconds: "5" }, billingMode: "per_second", unitPriceMicrocredits: 40_000 },
|
||||
{ selector: { operation: "image_to_video", vquality: "720p", videoSeconds: "5", imageCount: "1" }, billingMode: "per_second", unitPriceMicrocredits: 30_000 },
|
||||
],
|
||||
});
|
||||
const channel = resolveModelChannel(config, config.model);
|
||||
|
||||
expect(requestCreditCost({
|
||||
channelMode: "remote",
|
||||
modelCosts: channel.modelCosts,
|
||||
model: "agnes-video-2.5",
|
||||
capability: "video",
|
||||
config,
|
||||
requirements: textVideoRequirements,
|
||||
seconds: "5",
|
||||
})).toBe(0.125);
|
||||
});
|
||||
|
||||
test("uses reference count and operation to avoid a text-video price tier", () => {
|
||||
const config = systemConfig({
|
||||
tiers: [
|
||||
{ selector: { operation: "text_to_video", vquality: "720p", videoSeconds: "5" }, billingMode: "per_second", unitPriceMicrocredits: 25_000 },
|
||||
{ selector: { operation: "image_to_video", vquality: "720p", videoSeconds: "5", imageCount: "1" }, billingMode: "per_second", unitPriceMicrocredits: 30_000 },
|
||||
],
|
||||
});
|
||||
const requirements: ModelRequirements = {
|
||||
...textVideoRequirements,
|
||||
input: { ...textVideoRequirements.input!, imageCount: 1 },
|
||||
};
|
||||
const matched = priceTiersForCurrentSelection(resolveModelChannel(config, config.model).modelCosts![0]!.logicalPriceTiers!, "video", config, requirements);
|
||||
|
||||
expect(matched).toHaveLength(1);
|
||||
expect(matched[0]?.unitPriceMicrocredits).toBe(30_000);
|
||||
});
|
||||
|
||||
test("multiplies fixed image request pricing by output count", () => {
|
||||
const config = systemConfig({
|
||||
capability: "image",
|
||||
tiers: [{ selector: { quality: "high", size: "16:9" }, billingMode: "fixed_request", unitPriceMicrocredits: 10_000 }],
|
||||
});
|
||||
const channel = resolveModelChannel(config, config.model);
|
||||
|
||||
expect(requestCreditCost({
|
||||
channelMode: "remote",
|
||||
modelCosts: channel.modelCosts,
|
||||
model: "image-model",
|
||||
capability: "image",
|
||||
config,
|
||||
requirements: { capability: "image" },
|
||||
count: 3,
|
||||
})).toBe(0.03);
|
||||
});
|
||||
|
||||
test("builds a logical-model quote using the normalized current request", () => {
|
||||
const config = systemConfig({
|
||||
logicalModelId: "logical-video-1",
|
||||
tiers: [{ selector: {}, billingMode: "per_second", unitPriceMicrocredits: 25_000 }],
|
||||
});
|
||||
const quote = modelQuoteRequest(config, config.model, "video", textVideoRequirements);
|
||||
|
||||
expect(quote).toMatchObject({
|
||||
logicalModelID: "logical-video-1",
|
||||
intent: {
|
||||
capability: "video",
|
||||
operation: "text_to_video",
|
||||
inputs: { image: 0, video: 0, audio: 0 },
|
||||
options: { vquality: "720", videoSeconds: 5 },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user