feat(billing): 优化计费精度
- 新增账单配置 API,包括获取和更新显示小数位数的功能。 - 在前端页面中集成账单配置,允许用户设置金额显示精度。 - 更新相关页面(分析、定价、令牌、使用日志)以支持动态显示小数位数。 - 规范化账单金额处理,确保金额在前端和后端一致。
This commit is contained in:
1 parent
9b52d43835
commit
0258367a64
21 files changed
+587
-74
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user