feat(*): 模型生成链路 - 完善 Agnes 结果兼容、价格展示与请求日志追踪

This commit is contained in:
ddCat
2026-08-28 06:53:57 +08:00
parent 2c02f5ef3d
commit bd0d0fd4fd
20 changed files with 671 additions and 108 deletions

View File

@@ -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");

View 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 },
},
});
});
});