feat(billing): 优化计费精度

- 新增账单配置 API,包括获取和更新显示小数位数的功能。
- 在前端页面中集成账单配置,允许用户设置金额显示精度。
- 更新相关页面(分析、定价、令牌、使用日志)以支持动态显示小数位数。
- 规范化账单金额处理,确保金额在前端和后端一致。
This commit is contained in:
Jeff Gu committed 2026-04-13 13:56:13 +08:00
1 parent 9b52d43835
commit 0258367a64
21 files changed
+587 -74

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import { Context } from "hono";
import { OpenAPIRoute } from "chanfana";
import { z } from "zod";
import { CONSTANTS } from "../constants";
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
import { BillingConfig, normalizeBillingConfig } from "../billing";
import { getJsonSetting, saveSetting } from "../utils";
const billingConfigSchema = z.object({
displayDecimals: z.number().int().min(0).max(9),
});
export class BillingConfigGetEndpoint extends OpenAPIRoute {
schema = {
tags: ["Admin API"],
summary: "Get billing display configuration",
responses: {
...CommonSuccessfulResponse(billingConfigSchema),
...CommonErrorResponse,
},
};
async handle(c: Context<HonoCustomType>) {
const billingConfig = await getJsonSetting<BillingConfig>(
c,
CONSTANTS.BILLING_CONFIG_KEY
);
return {
success: true,
data: normalizeBillingConfig(billingConfig),
} as CommonResponse;
}
}
export class BillingConfigUpdateEndpoint extends OpenAPIRoute {
schema = {
tags: ["Admin API"],
summary: "Update billing display configuration",
request: {
body: {
content: {
"application/json": {
schema: billingConfigSchema,
},
},
},
},
responses: {
...CommonSuccessfulResponse(billingConfigSchema),
...CommonErrorResponse,
},
};
async handle(c: Context<HonoCustomType>) {
const body = await c.req.json<BillingConfig>();
const config = normalizeBillingConfig(body);
await saveSetting(
c,
CONSTANTS.BILLING_CONFIG_KEY,
JSON.stringify(config)
);
return {
success: true,
data: config,
message: "Billing config updated successfully",
} as CommonResponse;
}
}
+5
View File
@@ -11,6 +11,9 @@ import {
import {
PricingGetEndpoint, PricingUpdateEndpoint
} from "./pricing_api"
import {
BillingConfigGetEndpoint, BillingConfigUpdateEndpoint
} from "./billing_api"
import {
AnalyticsOverviewEndpoint,
AnalyticsTrendEndpoint,
@@ -54,6 +57,8 @@ api.delete("/api/admin/token/:key", TokenDeleteEndpoint)
// Pricing management routes
api.get("/api/admin/pricing", PricingGetEndpoint)
api.post("/api/admin/pricing", PricingUpdateEndpoint)
api.get("/api/admin/billing/config", BillingConfigGetEndpoint)
api.post("/api/admin/billing/config", BillingConfigUpdateEndpoint)
// Analytics management routes
api.get("/api/admin/analytics/overview", AnalyticsOverviewEndpoint)
+2 -2
View File
@@ -13,7 +13,7 @@ export class TokenListEndpoint extends OpenAPIRoute {
...CommonSuccessfulResponse(z.array(z.object({
key: z.string(),
value: z.string(),
usage: z.number(),
usage: z.number().int(),
created_at: z.string(),
updated_at: z.string(),
}))),
@@ -48,7 +48,7 @@ export class TokenUpsertEndpoint extends OpenAPIRoute {
schema: z.object({
name: z.string().describe('Token name'),
channel_keys: z.array(z.string()).describe('Channel keys to bind (empty array means access to all channels)'),
total_quota: z.number().describe('Total quota amount'),
total_quota: z.number().int().nonnegative().describe('Total quota amount in raw billing units'),
}),
},
},
+12 -4
View File
@@ -1,5 +1,9 @@
import { Context } from "hono";
import {
calculateRequestCostRaw,
calculateTokenRateCostRaw,
} from "../billing";
import { CONSTANTS } from "../constants";
import { getJsonSetting } from "../utils";
@@ -45,7 +49,7 @@ export const TokenUtils = {
): Promise<UsageCostResult> {
const pricing = await this.getPricing(c, model, targetChannelConfig);
const hasTokens = usage.prompt_tokens != null && usage.completion_tokens != null;
const requestCost = pricing?.request || 0;
const requestCost = calculateRequestCostRaw(pricing?.request || 0);
if (!pricing || (!hasTokens && requestCost <= 0)) {
return {
@@ -58,12 +62,16 @@ export const TokenUtils = {
};
}
const inputCost = hasTokens ? usage.prompt_tokens! * pricing.input : 0;
const outputCost = hasTokens ? usage.completion_tokens! * pricing.output : 0;
const inputCost = hasTokens
? calculateTokenRateCostRaw(usage.prompt_tokens!, pricing.input)
: 0;
const outputCost = hasTokens
? calculateTokenRateCostRaw(usage.completion_tokens!, pricing.output)
: 0;
let cacheCost = 0;
if (hasTokens && usage.cached_tokens && usage.cached_tokens > 0 && pricing.cache) {
cacheCost = usage.cached_tokens * pricing.cache;
cacheCost = calculateTokenRateCostRaw(usage.cached_tokens, pricing.cache);
}
return {