This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "../test/testHarness";
|
||||
|
||||
import {
|
||||
normalizePublicModelPrice,
|
||||
normalizePublicModelPrices,
|
||||
} from "./publicPricingClient";
|
||||
|
||||
describe("publicPricingClient", () => {
|
||||
it("normalizes camelCase public model price payloads", () => {
|
||||
expect(
|
||||
normalizePublicModelPrice({
|
||||
id: 1,
|
||||
modelKey: "gpt-4o",
|
||||
displayName: "GPT-4o",
|
||||
category: "text",
|
||||
pricingType: "token",
|
||||
inputPriceMills: 27,
|
||||
outputPriceMills: 108,
|
||||
flatPriceMills: null,
|
||||
currency: "CNY",
|
||||
enabled: true,
|
||||
}),
|
||||
).toEqual({
|
||||
id: 1,
|
||||
modelKey: "gpt-4o",
|
||||
displayName: "GPT-4o",
|
||||
category: "text",
|
||||
pricingType: "token",
|
||||
inputPriceMills: 27,
|
||||
outputPriceMills: 108,
|
||||
flatPriceMills: null,
|
||||
currency: "CNY",
|
||||
enabled: true,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes snake_case public model price payloads inside containers", () => {
|
||||
expect(
|
||||
normalizePublicModelPrices({
|
||||
prices: [
|
||||
{
|
||||
model_key: "deepseek-chat",
|
||||
display_name: "DeepSeek Chat",
|
||||
pricing_type: "token",
|
||||
input_price_mills: "2",
|
||||
output_price_mills: "8",
|
||||
flat_price_mills: "0",
|
||||
enabled: 1,
|
||||
},
|
||||
{ display_name: "missing key" },
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
id: undefined,
|
||||
modelKey: "deepseek-chat",
|
||||
displayName: "DeepSeek Chat",
|
||||
category: undefined,
|
||||
pricingType: "token",
|
||||
inputPriceMills: 2,
|
||||
outputPriceMills: 8,
|
||||
flatPriceMills: 0,
|
||||
currency: "CNY",
|
||||
enabled: true,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||||
import { isRecord, serverRequest } from "./serverConnection";
|
||||
|
||||
export interface PublicModelPrice {
|
||||
id?: number | string;
|
||||
modelKey: string;
|
||||
displayName?: string;
|
||||
category?: string;
|
||||
pricingType?: string;
|
||||
inputPriceMills: number | null;
|
||||
outputPriceMills: number | null;
|
||||
flatPriceMills: number | null;
|
||||
currency: string;
|
||||
enabled: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
function readString(
|
||||
record: Record<string, unknown>,
|
||||
keys: string[],
|
||||
): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readNumber(
|
||||
record: Record<string, unknown>,
|
||||
keys: string[],
|
||||
): number | null {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
const parsed =
|
||||
typeof value === "number"
|
||||
? value
|
||||
: typeof value === "string"
|
||||
? Number(value)
|
||||
: NaN;
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readBoolean(
|
||||
record: Record<string, unknown>,
|
||||
keys: string[],
|
||||
fallback: boolean,
|
||||
): boolean {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value !== 0;
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["1", "true", "yes", "enabled"].includes(normalized)) return true;
|
||||
if (["0", "false", "no", "disabled"].includes(normalized)) return false;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function normalizePublicModelPrice(
|
||||
raw: unknown,
|
||||
): PublicModelPrice | null {
|
||||
if (!isRecord(raw)) return null;
|
||||
|
||||
const modelKey = readString(raw, ["modelKey", "model_key", "key", "model"]);
|
||||
if (!modelKey) return null;
|
||||
|
||||
const displayName = readString(raw, ["displayName", "display_name", "name"]);
|
||||
const category = readString(raw, ["category", "type"]);
|
||||
const pricingType = readString(raw, ["pricingType", "pricing_type"]);
|
||||
const currency = readString(raw, ["currency"]) || "CNY";
|
||||
const createdAt = readString(raw, ["createdAt", "created_at"]);
|
||||
const updatedAt = readString(raw, ["updatedAt", "updated_at"]);
|
||||
const idValue = raw.id;
|
||||
|
||||
return {
|
||||
id:
|
||||
typeof idValue === "number" || typeof idValue === "string"
|
||||
? idValue
|
||||
: undefined,
|
||||
modelKey,
|
||||
displayName,
|
||||
category,
|
||||
pricingType,
|
||||
inputPriceMills: readNumber(raw, ["inputPriceMills", "input_price_mills"]),
|
||||
outputPriceMills: readNumber(raw, [
|
||||
"outputPriceMills",
|
||||
"output_price_mills",
|
||||
]),
|
||||
flatPriceMills: readNumber(raw, ["flatPriceMills", "flat_price_mills"]),
|
||||
currency,
|
||||
enabled: readBoolean(raw, ["enabled", "is_enabled"], true),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizePublicModelPrices(
|
||||
payload: unknown,
|
||||
): PublicModelPrice[] {
|
||||
const rawPrices = Array.isArray(payload)
|
||||
? payload
|
||||
: isRecord(payload) && Array.isArray(payload.prices)
|
||||
? payload.prices
|
||||
: isRecord(payload) && Array.isArray(payload.models)
|
||||
? payload.models
|
||||
: [];
|
||||
|
||||
return rawPrices
|
||||
.map((item) => normalizePublicModelPrice(item))
|
||||
.filter((item): item is PublicModelPrice => Boolean(item));
|
||||
}
|
||||
|
||||
let cachedPrices: PublicModelPrice[] | null = null;
|
||||
let pricesRouteMissing = false;
|
||||
|
||||
export const publicPricingClient = {
|
||||
async getPrices(): Promise<PublicModelPrice[]> {
|
||||
if (cachedPrices) return cachedPrices;
|
||||
if (pricesRouteMissing) return [];
|
||||
|
||||
try {
|
||||
const payload = await serverRequest<unknown>("prices", {
|
||||
fallbackMessage: "Model prices request failed",
|
||||
});
|
||||
cachedPrices = normalizePublicModelPrices(payload);
|
||||
return cachedPrices;
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
pricesRouteMissing = true;
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -40,6 +40,7 @@ import { useGenerationTasks } from "../../hooks/useGenerationTasks";
|
||||
|
||||
import { conversationClient, type ConversationSummary } from "../../api/conversationClient";
|
||||
import { modelCapabilitiesClient } from "../../api/modelCapabilitiesClient";
|
||||
import { publicPricingClient, type PublicModelPrice } from "../../api/publicPricingClient";
|
||||
import type { CreatePreviewTaskInput } from "../../api/webGenerationGateway";
|
||||
import type { WebProjectSummary } from "../../types";
|
||||
import {
|
||||
@@ -58,6 +59,8 @@ import {
|
||||
import { translateTaskError } from "../../utils/translateTaskError";
|
||||
import {
|
||||
buildLocalTimeoutMessage,
|
||||
FALLBACK_TEXT_TOKEN_CREDIT_RATE,
|
||||
formatTextTokenCreditRule,
|
||||
getTaskTimeoutPolicy,
|
||||
isTaskLocallyTimedOut,
|
||||
} from "../../utils/taskLifecycle";
|
||||
@@ -70,6 +73,7 @@ import { isViduModel } from "../../utils/viduRouting";
|
||||
import { isPixverseModel } from "../../utils/pixverseRouting";
|
||||
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||
import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
|
||||
import { resolveTextTokenCreditRate } from "../../utils/modelPricing";
|
||||
import {
|
||||
getImageQualityOptionsForContext,
|
||||
getDefaultImageQuality,
|
||||
@@ -403,9 +407,27 @@ function WorkbenchPage({
|
||||
const [videoQuality, setVideoQuality] = useState(() => getDefaultVideoQuality(VIDEO_MODEL_OPTIONS[0].value));
|
||||
|
||||
const [chatModel, setChatModel] = useState(CHAT_MODEL_OPTIONS[0].value);
|
||||
const [modelPrices, setModelPrices] = useState<PublicModelPrice[]>([]);
|
||||
const [thinkingSpeed, setThinkingSpeed] = useState(THINKING_SPEED_OPTIONS[0].value);
|
||||
const [thinkingDepth, setThinkingDepth] = useState(THINKING_DEPTH_OPTIONS[0].value);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
publicPricingClient
|
||||
.getPrices()
|
||||
.then((prices) => {
|
||||
if (!cancelled) setModelPrices(prices);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setModelPrices([]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -524,6 +546,10 @@ function WorkbenchPage({
|
||||
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
|
||||
|
||||
const imageSettingsSummary = `${imageRatio} / ${imageQuality}`;
|
||||
const selectedChatTokenRate = useMemo(
|
||||
() => resolveTextTokenCreditRate(modelPrices, chatModel) || FALLBACK_TEXT_TOKEN_CREDIT_RATE,
|
||||
[chatModel, modelPrices],
|
||||
);
|
||||
const billingEstimate = useMemo(() => {
|
||||
if (activeMode === "image") {
|
||||
return {
|
||||
@@ -552,9 +578,11 @@ function WorkbenchPage({
|
||||
};
|
||||
}
|
||||
}
|
||||
const textBillingPrefix =
|
||||
selectedChatTokenRate.source === "server" ? "文本计费" : "服务端价格暂不可用,按默认预估";
|
||||
return {
|
||||
label: "按 Token 结算",
|
||||
title: "文本对话按输入、输出 Token 实际用量结算,完成后显示本次积分",
|
||||
title: `${textBillingPrefix}:${activeModel},${formatTextTokenCreditRule(selectedChatTokenRate)}`,
|
||||
};
|
||||
}, [
|
||||
activeMode,
|
||||
@@ -562,6 +590,7 @@ function WorkbenchPage({
|
||||
activeModelValue,
|
||||
imageSettingsSummary,
|
||||
referenceItems,
|
||||
selectedChatTokenRate,
|
||||
videoDuration,
|
||||
videoQuality,
|
||||
videoQualityLabel,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from "../test/testHarness";
|
||||
|
||||
import {
|
||||
millsPerThousandTokensToCreditsPerMillion,
|
||||
modelPriceToTextTokenCreditRate,
|
||||
resolveTextTokenCreditRate,
|
||||
} from "./modelPricing";
|
||||
|
||||
describe("modelPricing", () => {
|
||||
it("converts backend mills per thousand tokens to credits per million tokens", () => {
|
||||
expect(millsPerThousandTokensToCreditsPerMillion(27)).toBe(2_700);
|
||||
expect(millsPerThousandTokensToCreditsPerMillion(108)).toBe(10_800);
|
||||
});
|
||||
|
||||
it("converts a token model price row to a text token credit rate", () => {
|
||||
expect(
|
||||
modelPriceToTextTokenCreditRate({
|
||||
modelKey: "gpt-4o",
|
||||
inputPriceMills: 27,
|
||||
outputPriceMills: 108,
|
||||
flatPriceMills: null,
|
||||
currency: "CNY",
|
||||
enabled: true,
|
||||
}),
|
||||
).toEqual({
|
||||
inputCreditsPerMillion: 2_700,
|
||||
outputCreditsPerMillion: 10_800,
|
||||
source: "server",
|
||||
modelKey: "gpt-4o",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves token pricing by exact or fuzzy model key without accepting flat prices", () => {
|
||||
const prices = [
|
||||
{
|
||||
modelKey: "gemini-3-pro-image",
|
||||
inputPriceMills: null,
|
||||
outputPriceMills: null,
|
||||
flatPriceMills: 200,
|
||||
currency: "CNY",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
modelKey: "gemini-3.1-pro",
|
||||
inputPriceMills: 12,
|
||||
outputPriceMills: 48,
|
||||
flatPriceMills: null,
|
||||
currency: "CNY",
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
expect(resolveTextTokenCreditRate(prices, "gemini")).toEqual({
|
||||
inputCreditsPerMillion: 1_200,
|
||||
outputCreditsPerMillion: 4_800,
|
||||
source: "server",
|
||||
modelKey: "gemini-3.1-pro",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { PublicModelPrice } from "../api/publicPricingClient";
|
||||
import type { TextTokenCreditRate } from "./taskLifecycle";
|
||||
|
||||
const TOKENS_PER_MILLION = 1_000_000;
|
||||
const BACKEND_TOKEN_PRICE_UNIT = 1_000;
|
||||
const CREDITS_PER_CNY = 100;
|
||||
const MILLS_PER_CNY = 1_000;
|
||||
const CREDITS_PER_MILL = CREDITS_PER_CNY / MILLS_PER_CNY;
|
||||
|
||||
function isUsablePrice(value: number | null | undefined): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
||||
}
|
||||
|
||||
function normalizeModelKey(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function compactModelKey(value: string): string {
|
||||
return normalizeModelKey(value).replace(/[^a-z0-9]+/g, "");
|
||||
}
|
||||
|
||||
function addCandidate(
|
||||
candidates: PublicModelPrice[],
|
||||
seen: Set<string>,
|
||||
price: PublicModelPrice,
|
||||
): void {
|
||||
const key = normalizeModelKey(price.modelKey);
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
candidates.push(price);
|
||||
}
|
||||
|
||||
export function millsPerThousandTokensToCreditsPerMillion(
|
||||
priceMills: number,
|
||||
): number {
|
||||
if (!isUsablePrice(priceMills)) return 0;
|
||||
return (
|
||||
priceMills *
|
||||
(TOKENS_PER_MILLION / BACKEND_TOKEN_PRICE_UNIT) *
|
||||
CREDITS_PER_MILL
|
||||
);
|
||||
}
|
||||
|
||||
export function modelPriceToTextTokenCreditRate(
|
||||
price: PublicModelPrice,
|
||||
): TextTokenCreditRate | null {
|
||||
if (
|
||||
!isUsablePrice(price.inputPriceMills) ||
|
||||
!isUsablePrice(price.outputPriceMills)
|
||||
)
|
||||
return null;
|
||||
|
||||
return {
|
||||
inputCreditsPerMillion: millsPerThousandTokensToCreditsPerMillion(
|
||||
price.inputPriceMills,
|
||||
),
|
||||
outputCreditsPerMillion: millsPerThousandTokensToCreditsPerMillion(
|
||||
price.outputPriceMills,
|
||||
),
|
||||
source: "server",
|
||||
modelKey: price.modelKey,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTextTokenCreditRate(
|
||||
prices: PublicModelPrice[],
|
||||
modelKey: string | null | undefined,
|
||||
): TextTokenCreditRate | null {
|
||||
const normalizedTarget = normalizeModelKey(modelKey || "");
|
||||
if (!normalizedTarget) return null;
|
||||
|
||||
const compactTarget = compactModelKey(normalizedTarget);
|
||||
const candidates: PublicModelPrice[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const price of prices) {
|
||||
if (normalizeModelKey(price.modelKey) === normalizedTarget) {
|
||||
addCandidate(candidates, seen, price);
|
||||
}
|
||||
}
|
||||
|
||||
for (const price of prices) {
|
||||
if (compactModelKey(price.modelKey) === compactTarget) {
|
||||
addCandidate(candidates, seen, price);
|
||||
}
|
||||
}
|
||||
|
||||
for (const price of prices) {
|
||||
const compactPriceKey = compactModelKey(price.modelKey);
|
||||
if (
|
||||
compactPriceKey.includes(compactTarget) ||
|
||||
compactTarget.includes(compactPriceKey)
|
||||
) {
|
||||
addCandidate(candidates, seen, price);
|
||||
}
|
||||
}
|
||||
|
||||
for (const price of candidates) {
|
||||
const rate = modelPriceToTextTokenCreditRate(price);
|
||||
if (rate) return rate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
TEXT_INPUT_CREDITS_PER_MILLION,
|
||||
TEXT_OUTPUT_CREDITS_PER_MILLION,
|
||||
estimateTextTokenCredits,
|
||||
formatTextTokenCreditRule,
|
||||
getTaskTimeoutPolicy,
|
||||
isTaskLocallyTimedOut,
|
||||
} from "./taskLifecycle";
|
||||
|
||||
describe("taskLifecycle", () => {
|
||||
it("keeps text token billing at 1 CNY to 100 credits", () => {
|
||||
it("keeps fallback text token billing at 1 CNY to 100 credits", () => {
|
||||
expect(TEXT_INPUT_CREDITS_PER_MILLION).toBe(200);
|
||||
expect(TEXT_OUTPUT_CREDITS_PER_MILLION).toBe(500);
|
||||
expect(
|
||||
@@ -20,6 +21,23 @@ describe("taskLifecycle", () => {
|
||||
).toBe(700);
|
||||
});
|
||||
|
||||
it("estimates text billing from dynamic server pricing rates", () => {
|
||||
expect(
|
||||
estimateTextTokenCredits(
|
||||
{
|
||||
promptTokens: 1_000_000,
|
||||
completionTokens: 1_000_000,
|
||||
},
|
||||
{
|
||||
inputCreditsPerMillion: 2_700,
|
||||
outputCreditsPerMillion: 10_800,
|
||||
source: "server",
|
||||
modelKey: "gpt-4o",
|
||||
},
|
||||
),
|
||||
).toBe(13_500);
|
||||
});
|
||||
|
||||
it("ignores negative token counts when estimating text billing", () => {
|
||||
expect(
|
||||
estimateTextTokenCredits({
|
||||
@@ -29,6 +47,17 @@ describe("taskLifecycle", () => {
|
||||
).toBe(250);
|
||||
});
|
||||
|
||||
it("formats text billing rules from the selected rate", () => {
|
||||
expect(
|
||||
formatTextTokenCreditRule({
|
||||
inputCreditsPerMillion: 2_700,
|
||||
outputCreditsPerMillion: 10_800,
|
||||
}),
|
||||
).toBe(
|
||||
"输入 Token 每百万 2,700 积分,输出 Token 每百万 10,800 积分,实际以服务端结算为准。",
|
||||
);
|
||||
});
|
||||
|
||||
it("marks unstarted tasks locally timed out after submit timeout", () => {
|
||||
const policy = getTaskTimeoutPolicy({ kind: "image" });
|
||||
|
||||
|
||||
@@ -32,11 +32,24 @@ export interface TextTokenUsage {
|
||||
totalTokens?: number;
|
||||
}
|
||||
|
||||
export interface TextTokenCreditRate {
|
||||
inputCreditsPerMillion: number;
|
||||
outputCreditsPerMillion: number;
|
||||
source?: "server" | "fallback";
|
||||
modelKey?: string;
|
||||
}
|
||||
|
||||
const CREDITS_PER_CNY = 100;
|
||||
|
||||
export const TEXT_INPUT_CREDITS_PER_MILLION = 2 * CREDITS_PER_CNY;
|
||||
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5 * CREDITS_PER_CNY;
|
||||
|
||||
export const FALLBACK_TEXT_TOKEN_CREDIT_RATE: TextTokenCreditRate = {
|
||||
inputCreditsPerMillion: TEXT_INPUT_CREDITS_PER_MILLION,
|
||||
outputCreditsPerMillion: TEXT_OUTPUT_CREDITS_PER_MILLION,
|
||||
source: "fallback",
|
||||
};
|
||||
|
||||
const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = {
|
||||
submitTimeoutMs: 90_000,
|
||||
noProgressTimeoutMs: 120_000,
|
||||
@@ -145,18 +158,42 @@ export function getRefundHint(status: TaskRefundStatus): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function estimateTextTokenCredits(usage: TextTokenUsage): number {
|
||||
const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
|
||||
const completionTokens = Math.max(0, Number(usage.completionTokens || 0));
|
||||
return (promptTokens / 1_000_000) * TEXT_INPUT_CREDITS_PER_MILLION +
|
||||
(completionTokens / 1_000_000) * TEXT_OUTPUT_CREDITS_PER_MILLION;
|
||||
function sanitizeCreditRate(value: number): number {
|
||||
return Number.isFinite(value) && value >= 0 ? value : 0;
|
||||
}
|
||||
|
||||
export function formatTextTokenUsage(usage?: TextTokenUsage | null): string {
|
||||
const rule = "文本计费规则:输入 Token 每百万 200 积分,输出 Token 每百万 500 积分,实际以服务端结算为准。";
|
||||
function formatCreditRate(value: number): string {
|
||||
const safeValue = sanitizeCreditRate(value);
|
||||
if (safeValue >= 100) return Math.round(safeValue).toLocaleString("zh-CN");
|
||||
return Number(safeValue.toFixed(4)).toString();
|
||||
}
|
||||
|
||||
export function formatTextTokenCreditRule(
|
||||
rate: TextTokenCreditRate = FALLBACK_TEXT_TOKEN_CREDIT_RATE,
|
||||
): string {
|
||||
return `输入 Token 每百万 ${formatCreditRate(rate.inputCreditsPerMillion)} 积分,输出 Token 每百万 ${formatCreditRate(rate.outputCreditsPerMillion)} 积分,实际以服务端结算为准。`;
|
||||
}
|
||||
|
||||
export function estimateTextTokenCredits(
|
||||
usage: TextTokenUsage,
|
||||
rate: TextTokenCreditRate = FALLBACK_TEXT_TOKEN_CREDIT_RATE,
|
||||
): number {
|
||||
const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
|
||||
const completionTokens = Math.max(0, Number(usage.completionTokens || 0));
|
||||
return (
|
||||
(promptTokens / 1_000_000) * sanitizeCreditRate(rate.inputCreditsPerMillion) +
|
||||
(completionTokens / 1_000_000) * sanitizeCreditRate(rate.outputCreditsPerMillion)
|
||||
);
|
||||
}
|
||||
|
||||
export function formatTextTokenUsage(
|
||||
usage?: TextTokenUsage | null,
|
||||
rate: TextTokenCreditRate = FALLBACK_TEXT_TOKEN_CREDIT_RATE,
|
||||
): string {
|
||||
const rule = `文本计费规则:${formatTextTokenCreditRule(rate)}`;
|
||||
if (!usage) return rule;
|
||||
const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
|
||||
const completionTokens = Math.max(0, Number(usage.completionTokens || 0));
|
||||
const estimatedCredits = estimateTextTokenCredits({ promptTokens, completionTokens });
|
||||
const estimatedCredits = estimateTextTokenCredits({ promptTokens, completionTokens }, rate);
|
||||
return `本次 Token:输入 ${promptTokens},输出 ${completionTokens},预估 ${estimatedCredits.toFixed(4)} 积分。\n${rule}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user