fix: 修正 Sub2API 利润币种语义
This commit is contained in:
@@ -210,9 +210,9 @@ function readProfitConfig(value: unknown): CodexPoolProfitConfig {
|
||||
if (new Set(allGroups).size !== allGroups.length) {
|
||||
throw new Error(`${codexPoolConfigPath}.profit.groups entries must be disjoint`);
|
||||
}
|
||||
const netRevenueCoefficient = numberValue(value.netRevenueCoefficient);
|
||||
if (netRevenueCoefficient === null || netRevenueCoefficient < 0) {
|
||||
throw new Error(`${codexPoolConfigPath}.profit.netRevenueCoefficient must be >= 0`);
|
||||
const saleRateCnyPerApiUsd = numberValue(value.saleRateCnyPerApiUsd);
|
||||
if (saleRateCnyPerApiUsd === null || saleRateCnyPerApiUsd < 0) {
|
||||
throw new Error(`${codexPoolConfigPath}.profit.saleRateCnyPerApiUsd must be >= 0`);
|
||||
}
|
||||
const defaultWindow = requiredStringConfigField(value, "defaultWindow", "profit");
|
||||
if (!/^[1-9][0-9]*(?:s|m|h|d)$/u.test(defaultWindow)) {
|
||||
@@ -223,14 +223,14 @@ function readProfitConfig(value: unknown): CodexPoolProfitConfig {
|
||||
throw new Error(`${codexPoolConfigPath}.profit.usageCostField must be actual_cost`);
|
||||
}
|
||||
const accountCostSource = requiredStringConfigField(value, "accountCostSource", "profit");
|
||||
if (accountCostSource !== "account-name-trailing-coefficient") {
|
||||
throw new Error(`${codexPoolConfigPath}.profit.accountCostSource must be account-name-trailing-coefficient`);
|
||||
if (accountCostSource !== "account-name-trailing-cny-per-api-usd") {
|
||||
throw new Error(`${codexPoolConfigPath}.profit.accountCostSource must be account-name-trailing-cny-per-api-usd`);
|
||||
}
|
||||
return {
|
||||
defaultWindow,
|
||||
usageCostField,
|
||||
accountCostSource,
|
||||
netRevenueCoefficient,
|
||||
saleRateCnyPerApiUsd,
|
||||
groups: { profitable, selfUse, excluded },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,24 +91,24 @@ function renderProfit(response: Record<string, unknown>): RenderedCliResult {
|
||||
const lines = [
|
||||
`SUB2API PROFIT status=${text(report.status)} target=${text(record(response.target).id)} window=${text(window.since)}`,
|
||||
`WINDOW ${text(window.startAt)} -> ${text(window.endAt)} SOURCE native-admin-usage/${text(record(report.source).usageCostField)}`,
|
||||
`POLICY net-revenue=${text(policy.netRevenueCoefficient)} account-cost=${text(policy.accountCostSource)}`,
|
||||
`POLICY sale-rate=${text(policy.saleRateCnyPerApiUsd)}CNY/API_USD account-cost=${text(policy.accountCostSource)}`,
|
||||
"",
|
||||
"ACCOUNTS",
|
||||
];
|
||||
const accounts = arrayOfRecords(report.accounts);
|
||||
lines.push(table(
|
||||
["GROUP", "CLASS", "ACCOUNT", "ID", "REQ", "TOKENS", "API_AMOUNT", "COST_RATE", "REVENUE", "UPSTREAM_COST", "CONTRIBUTION", "STATUS"],
|
||||
["GROUP", "CLASS", "ACCOUNT", "ID", "REQ", "TOKENS", "API_USD", "COST_CNY/USD", "REVENUE_CNY", "UPSTREAM_CNY", "PROFIT_CNY", "STATUS"],
|
||||
accounts.map((row) => [
|
||||
text(row.groupName), text(row.class), text(row.accountName), text(row.accountId), text(row.requestCount), compactTokens(row.tokenCount),
|
||||
usd(row.apiAmountUsd), text(row.costCoefficient), usd(row.revenueUsd), usd(row.upstreamCostUsd), usd(row.profitContributionUsd), text(row.completeness),
|
||||
apiUsd(row.apiAmountUsd), text(row.costRateCnyPerApiUsd), cny(row.revenueCny), cny(row.upstreamCostCny), cny(row.profitContributionCny), text(row.completeness),
|
||||
]),
|
||||
));
|
||||
lines.push("", "PROFIT");
|
||||
lines.push(table(
|
||||
["SCOPE", "STATUS", "REVENUE", "PROFIT_POOL_COST", "SELF_USE_COST", "PROFIT"],
|
||||
["SCOPE", "STATUS", "REVENUE_CNY", "PROFIT_POOL_CNY", "SELF_USE_CNY", "PROFIT_CNY"],
|
||||
[
|
||||
["不扣自用", text(record(summary.withoutSelfUse).status), usd(summary.revenueUsd), usd(summary.profitPoolCostUsd), "-", usd(record(summary.withoutSelfUse).profitUsd)],
|
||||
["扣除自用", text(record(summary.withSelfUse).status), usd(summary.revenueUsd), usd(summary.profitPoolCostUsd), usd(summary.selfUseCostUsd), usd(record(summary.withSelfUse).profitUsd)],
|
||||
["不扣自用", text(record(summary.withoutSelfUse).status), cny(summary.revenueCny), cny(summary.profitPoolCostCny), "-", cny(record(summary.withoutSelfUse).profitCny)],
|
||||
["扣除自用", text(record(summary.withSelfUse).status), cny(summary.revenueCny), cny(summary.profitPoolCostCny), cny(summary.selfUseCostCny), cny(record(summary.withSelfUse).profitCny)],
|
||||
],
|
||||
));
|
||||
const missingCosts = arrayOfRecords(completeness.missingCostAccounts);
|
||||
@@ -161,12 +161,18 @@ function compactTokens(value: unknown): string {
|
||||
return String(numeric);
|
||||
}
|
||||
|
||||
function usd(value: unknown): string {
|
||||
function apiUsd(value: unknown): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? `$${numeric.toFixed(6)}` : text(value);
|
||||
}
|
||||
|
||||
function cny(value: unknown): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? `¥${numeric.toFixed(6)}` : text(value);
|
||||
}
|
||||
|
||||
function table(headers: string[], rows: string[][]): string {
|
||||
if (rows.length === 0) return `${headers.join(" ")}\n-`;
|
||||
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
|
||||
|
||||
@@ -112,7 +112,7 @@ def run_profit_report():
|
||||
config = payload.get("config") if isinstance(payload.get("config"), dict) else {}
|
||||
window = payload.get("since")
|
||||
cost_field = config.get("usageCostField")
|
||||
net_rate = profit_decimal(config.get("netRevenueCoefficient"))
|
||||
sale_rate = profit_decimal(config.get("saleRateCnyPerApiUsd"))
|
||||
end_at = datetime.now(timezone.utc)
|
||||
start_at = profit_window_start(window, end_at)
|
||||
_, token, _ = login()
|
||||
@@ -149,14 +149,14 @@ def run_profit_report():
|
||||
for aggregate in aggregates.values():
|
||||
rate = profit_account_rate(aggregate["accountName"])
|
||||
amount = aggregate.pop("apiAmount")
|
||||
revenue = amount * net_rate if group_class == "profit" else profit_decimal(0)
|
||||
revenue = amount * sale_rate if group_class == "profit" else profit_decimal(0)
|
||||
upstream_cost = amount * rate if rate is not None and group_class in ("profit", "self-use") else profit_decimal(0)
|
||||
aggregate["tokenCount"] = aggregate.pop("inputTokens") + aggregate.pop("outputTokens")
|
||||
aggregate["apiAmountUsd"] = profit_decimal_text(amount)
|
||||
aggregate["costCoefficient"] = profit_decimal_text(rate) if rate is not None else None
|
||||
aggregate["revenueUsd"] = profit_decimal_text(revenue)
|
||||
aggregate["upstreamCostUsd"] = profit_decimal_text(upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None
|
||||
aggregate["profitContributionUsd"] = profit_decimal_text(revenue - upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None
|
||||
aggregate["costRateCnyPerApiUsd"] = profit_decimal_text(rate) if rate is not None else None
|
||||
aggregate["revenueCny"] = profit_decimal_text(revenue)
|
||||
aggregate["upstreamCostCny"] = profit_decimal_text(upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None
|
||||
aggregate["profitContributionCny"] = profit_decimal_text(revenue - upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None
|
||||
aggregate["completeness"] = "complete" if rate is not None or group_class not in ("profit", "self-use") else "partial"
|
||||
account_rows.append(aggregate)
|
||||
group_amount += amount
|
||||
@@ -175,9 +175,9 @@ def run_profit_report():
|
||||
missing_profit = [row for row in profit_accounts if row["completeness"] != "complete"]
|
||||
missing_self = [row for row in self_accounts if row["completeness"] != "complete"]
|
||||
unclassified = [row for row in account_rows if row["class"] == "unclassified" and row["requestCount"] > 0]
|
||||
revenue = sum((profit_decimal(row["revenueUsd"]) for row in profit_accounts), profit_decimal(0))
|
||||
profit_cost = sum((profit_decimal(row["upstreamCostUsd"]) for row in profit_accounts if row["upstreamCostUsd"] is not None), profit_decimal(0))
|
||||
self_cost = sum((profit_decimal(row["upstreamCostUsd"]) for row in self_accounts if row["upstreamCostUsd"] is not None), profit_decimal(0))
|
||||
revenue = sum((profit_decimal(row["revenueCny"]) for row in profit_accounts), profit_decimal(0))
|
||||
profit_cost = sum((profit_decimal(row["upstreamCostCny"]) for row in profit_accounts if row["upstreamCostCny"] is not None), profit_decimal(0))
|
||||
self_cost = sum((profit_decimal(row["upstreamCostCny"]) for row in self_accounts if row["upstreamCostCny"] is not None), profit_decimal(0))
|
||||
without_self = revenue - profit_cost
|
||||
with_self = without_self - self_cost
|
||||
without_complete = not missing_profit and not unclassified
|
||||
@@ -193,11 +193,11 @@ def run_profit_report():
|
||||
"groups": group_rows,
|
||||
"accounts": account_rows,
|
||||
"summary": {
|
||||
"revenueUsd": profit_decimal_text(revenue),
|
||||
"profitPoolCostUsd": profit_decimal_text(profit_cost),
|
||||
"selfUseCostUsd": profit_decimal_text(self_cost),
|
||||
"withoutSelfUse": {"status": "complete" if without_complete else "partial", "profitUsd": profit_decimal_text(without_self) if without_complete else None},
|
||||
"withSelfUse": {"status": "complete" if with_complete else "partial", "profitUsd": profit_decimal_text(with_self) if with_complete else None},
|
||||
"revenueCny": profit_decimal_text(revenue),
|
||||
"profitPoolCostCny": profit_decimal_text(profit_cost),
|
||||
"selfUseCostCny": profit_decimal_text(self_cost),
|
||||
"withoutSelfUse": {"status": "complete" if without_complete else "partial", "profitCny": profit_decimal_text(without_self) if without_complete else None},
|
||||
"withSelfUse": {"status": "complete" if with_complete else "partial", "profitCny": profit_decimal_text(with_self) if with_complete else None},
|
||||
},
|
||||
"completeness": {
|
||||
"missingCostAccounts": [{"groupName": row["groupName"], "accountId": row["accountId"], "accountName": row["accountName"]} for row in missing_profit + missing_self],
|
||||
|
||||
@@ -184,8 +184,8 @@ export interface CodexRuntimeConfig {
|
||||
export interface CodexPoolProfitConfig {
|
||||
defaultWindow: string;
|
||||
usageCostField: "actual_cost";
|
||||
accountCostSource: "account-name-trailing-coefficient";
|
||||
netRevenueCoefficient: number;
|
||||
accountCostSource: "account-name-trailing-cny-per-api-usd";
|
||||
saleRateCnyPerApiUsd: number;
|
||||
groups: {
|
||||
profitable: string[];
|
||||
selfUse: string[];
|
||||
|
||||
Reference in New Issue
Block a user