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 {
|
||||
|
||||
+26
-5
@@ -1,5 +1,6 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import { BILLING_RAW_SCALE, LEGACY_TO_RAW_FACTOR } from "../billing";
|
||||
import { DEFAULT_USAGE_ANALYTICS_DATASET_NAME } from "./usage-logger";
|
||||
|
||||
export type AnalyticsRange = "24h" | "7d" | "30d" | "90d";
|
||||
@@ -129,6 +130,7 @@ const DOUBLE_FIELDS = {
|
||||
retryCount: "double7",
|
||||
upstreamStatus: "double8",
|
||||
successFlag: "double9",
|
||||
billingScale: "double10",
|
||||
} as const;
|
||||
|
||||
const BREAKDOWN_FIELDS: Record<AnalyticsBreakdownDimension, string> = {
|
||||
@@ -194,6 +196,7 @@ const USAGE_LOG_NUMERIC_COLUMNS = [
|
||||
DOUBLE_FIELDS.retryCount,
|
||||
DOUBLE_FIELDS.upstreamStatus,
|
||||
DOUBLE_FIELDS.successFlag,
|
||||
DOUBLE_FIELDS.billingScale,
|
||||
];
|
||||
|
||||
const ALL_PROBED_COLUMNS = [
|
||||
@@ -535,6 +538,18 @@ const buildDoubleSelect = (
|
||||
: `0 AS ${alias}`;
|
||||
};
|
||||
|
||||
const buildNormalizedTotalCostExpression = (support: DatasetColumnSupport): string => {
|
||||
if (!isColumnAvailable(support, DOUBLE_FIELDS.totalCost)) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
if (!isColumnAvailable(support, DOUBLE_FIELDS.billingScale)) {
|
||||
return `${DOUBLE_FIELDS.totalCost} * ${LEGACY_TO_RAW_FACTOR}`;
|
||||
}
|
||||
|
||||
return `if(${DOUBLE_FIELDS.billingScale} > 0, ${DOUBLE_FIELDS.totalCost} * (${BILLING_RAW_SCALE} / ${DOUBLE_FIELDS.billingScale}), ${DOUBLE_FIELDS.totalCost} * ${LEGACY_TO_RAW_FACTOR})`;
|
||||
};
|
||||
|
||||
const hasAnyLegacyLogSchema = (support: DatasetColumnSupport): boolean => {
|
||||
return LEGACY_BLOB_COLUMNS.some((columnName) => isColumnAvailable(support, columnName));
|
||||
};
|
||||
@@ -608,11 +623,13 @@ export const queryUsageOverview = async (
|
||||
) => {
|
||||
const { range, config } = getRangeConfig(requestedRange);
|
||||
const dataset = getDatasetName(c);
|
||||
const columnSupport = await getDatasetColumnSupport(c, dataset, buildRangeWhereClause(config));
|
||||
const normalizedTotalCostExpression = buildNormalizedTotalCostExpression(columnSupport);
|
||||
const rows = await runAnalyticsQuery<Record<string, unknown>>(c, `
|
||||
SELECT
|
||||
sum(_sample_interval) AS requests,
|
||||
sum(${DOUBLE_FIELDS.successFlag} * _sample_interval) AS successes,
|
||||
sum(${DOUBLE_FIELDS.totalCost} * _sample_interval) AS total_cost,
|
||||
sum(${normalizedTotalCostExpression} * _sample_interval) AS total_cost,
|
||||
sum(${DOUBLE_FIELDS.totalTokens} * _sample_interval) AS total_tokens,
|
||||
sum(${DOUBLE_FIELDS.promptTokens} * _sample_interval) AS prompt_tokens,
|
||||
sum(${DOUBLE_FIELDS.completionTokens} * _sample_interval) AS completion_tokens,
|
||||
@@ -648,12 +665,14 @@ export const queryUsageTrend = async (
|
||||
const { range, config } = getRangeConfig(requestedRange);
|
||||
const dataset = getDatasetName(c);
|
||||
const trendWindow = buildTrendWindow(range, config);
|
||||
const columnSupport = await getDatasetColumnSupport(c, dataset, trendWindow.whereClause);
|
||||
const normalizedTotalCostExpression = buildNormalizedTotalCostExpression(columnSupport);
|
||||
const rows = await runAnalyticsQuery<Record<string, unknown>>(c, `
|
||||
SELECT
|
||||
toUnixTimestamp(${buildBucketClause(config)}) AS bucket_ts,
|
||||
sum(_sample_interval) AS requests,
|
||||
sum(${DOUBLE_FIELDS.successFlag} * _sample_interval) AS successes,
|
||||
sum(${DOUBLE_FIELDS.totalCost} * _sample_interval) AS total_cost
|
||||
sum(${normalizedTotalCostExpression} * _sample_interval) AS total_cost
|
||||
FROM ${dataset}
|
||||
WHERE ${trendWindow.whereClause}
|
||||
GROUP BY bucket_ts
|
||||
@@ -696,12 +715,14 @@ export const queryUsageBreakdown = async (
|
||||
: "token") as AnalyticsBreakdownDimension;
|
||||
const dataset = getDatasetName(c);
|
||||
const dimensionField = BREAKDOWN_FIELDS[dimension];
|
||||
const columnSupport = await getDatasetColumnSupport(c, dataset, buildRangeWhereClause(config));
|
||||
const normalizedTotalCostExpression = buildNormalizedTotalCostExpression(columnSupport);
|
||||
const rows = await runAnalyticsQuery<Record<string, unknown>>(c, `
|
||||
SELECT
|
||||
${dimensionField} AS label,
|
||||
sum(_sample_interval) AS requests,
|
||||
sum(${DOUBLE_FIELDS.successFlag} * _sample_interval) AS successes,
|
||||
sum(${DOUBLE_FIELDS.totalCost} * _sample_interval) AS total_cost,
|
||||
sum(${normalizedTotalCostExpression} * _sample_interval) AS total_cost,
|
||||
sum(${DOUBLE_FIELDS.promptTokens} * _sample_interval) AS prompt_tokens,
|
||||
sum(${DOUBLE_FIELDS.completionTokens} * _sample_interval) AS completion_tokens,
|
||||
sum(${DOUBLE_FIELDS.latencyMs} * _sample_interval) / sum(_sample_interval) AS avg_latency_ms
|
||||
@@ -789,7 +810,7 @@ SELECT
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.completionTokens, "completion_tokens")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.cachedTokens, "cached_tokens")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.totalTokens, "total_tokens")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.totalCost, "total_cost")},
|
||||
${buildNormalizedTotalCostExpression(columnSupport)} AS total_cost,
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.latencyMs, "latency_ms")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.retryCount, "retry_count")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.upstreamStatus, "upstream_status")}
|
||||
@@ -962,7 +983,7 @@ SELECT
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.completionTokens, "completion_tokens")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.cachedTokens, "cached_tokens")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.totalTokens, "total_tokens")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.totalCost, "total_cost")},
|
||||
${buildNormalizedTotalCostExpression(columnSupport)} AS total_cost,
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.latencyMs, "latency_ms")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.retryCount, "retry_count")},
|
||||
${buildDoubleSelect(columnSupport, DOUBLE_FIELDS.upstreamStatus, "upstream_status")}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Context } from "hono";
|
||||
import { BILLING_RAW_SCALE } from "../billing";
|
||||
|
||||
export const DEFAULT_USAGE_ANALYTICS_DATASET_NAME = "usage_events_by_token";
|
||||
|
||||
@@ -234,6 +235,7 @@ const buildCommonPoint = (
|
||||
safeNumber(context.trackingState.retryCount),
|
||||
upstreamStatus,
|
||||
result === "success" ? 1 : 0,
|
||||
BILLING_RAW_SCALE,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
export const BILLING_RAW_SCALE = 1_000_000_000;
|
||||
export const LEGACY_BILLING_SCALE = 1_000_000;
|
||||
export const LEGACY_TO_RAW_FACTOR = BILLING_RAW_SCALE / LEGACY_BILLING_SCALE;
|
||||
|
||||
export const DEFAULT_BILLING_DISPLAY_DECIMALS = 6;
|
||||
export const MIN_BILLING_DISPLAY_DECIMALS = 0;
|
||||
export const MAX_BILLING_DISPLAY_DECIMALS = 9;
|
||||
|
||||
export type BillingConfig = {
|
||||
displayDecimals: number;
|
||||
}
|
||||
|
||||
const toFiniteNumber = (value: unknown): number => {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const normalizeRawBillingAmount = (value: unknown): number => {
|
||||
return Math.max(0, Math.round(toFiniteNumber(value)));
|
||||
};
|
||||
|
||||
export const legacyBillingAmountToRaw = (value: unknown): number => {
|
||||
return normalizeRawBillingAmount(toFiniteNumber(value) * LEGACY_TO_RAW_FACTOR);
|
||||
};
|
||||
|
||||
export const calculateTokenRateCostRaw = (tokens: unknown, rate: unknown): number => {
|
||||
return normalizeRawBillingAmount(toFiniteNumber(tokens) * toFiniteNumber(rate) * LEGACY_TO_RAW_FACTOR);
|
||||
};
|
||||
|
||||
export const calculateRequestCostRaw = (value: unknown): number => {
|
||||
return legacyBillingAmountToRaw(value);
|
||||
};
|
||||
|
||||
export const normalizeBillingDisplayDecimals = (value: unknown): number => {
|
||||
const parsed = Math.round(toFiniteNumber(value));
|
||||
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return DEFAULT_BILLING_DISPLAY_DECIMALS;
|
||||
}
|
||||
|
||||
return Math.min(
|
||||
MAX_BILLING_DISPLAY_DECIMALS,
|
||||
Math.max(MIN_BILLING_DISPLAY_DECIMALS, parsed)
|
||||
);
|
||||
};
|
||||
|
||||
export const normalizeBillingConfig = (value: Partial<BillingConfig> | null | undefined): BillingConfig => {
|
||||
return {
|
||||
displayDecimals: normalizeBillingDisplayDecimals(value?.displayDecimals),
|
||||
};
|
||||
};
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: "v0.0.1",
|
||||
DB_VERSION_KEY: "db_version",
|
||||
DB_VERSION: "v0.0.5",
|
||||
DB_VERSION: "v0.0.6",
|
||||
MODEL_PRICING_KEY: "MODEL_PRICING_CONFIG",
|
||||
BILLING_CONFIG_KEY: "BILLING_CONFIG",
|
||||
}
|
||||
+113
-1
@@ -1,4 +1,5 @@
|
||||
import { Context } from "hono";
|
||||
import { legacyBillingAmountToRaw } from "../billing";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getSetting, saveSetting } from "../utils";
|
||||
import { sanitizeChannelConfig } from "../channel-config";
|
||||
@@ -13,7 +14,7 @@ CREATE TABLE IF NOT EXISTS channel_config (
|
||||
CREATE TABLE IF NOT EXISTS api_token (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
usage REAL DEFAULT 0,
|
||||
usage INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -33,10 +34,119 @@ const getInitQuery = () => DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
||||
.filter(Boolean)
|
||||
.join(";\n");
|
||||
|
||||
const toSingleStatement = (sql: string) => {
|
||||
return sql
|
||||
.replace(/[\r\n]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.replace(/;?$/, ";");
|
||||
};
|
||||
|
||||
const initializeSchema = async (c: Context<HonoCustomType>) => {
|
||||
await c.env.DB.exec(getInitQuery());
|
||||
};
|
||||
|
||||
type TableInfoRow = {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
type SqliteMasterRow = {
|
||||
name: string;
|
||||
}
|
||||
|
||||
type LegacyTokenRow = {
|
||||
key: string;
|
||||
value: string;
|
||||
usage: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
const doesTableExist = async (c: Context<HonoCustomType>, tableName: string): Promise<boolean> => {
|
||||
const table = await c.env.DB.prepare(
|
||||
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`
|
||||
).bind(tableName).first<SqliteMasterRow>();
|
||||
|
||||
return Boolean(table?.name);
|
||||
};
|
||||
|
||||
const getApiTokenUsageColumnType = async (c: Context<HonoCustomType>): Promise<string> => {
|
||||
const tableInfo = await c.env.DB.prepare(
|
||||
`PRAGMA table_info(api_token)`
|
||||
).all<TableInfoRow>();
|
||||
|
||||
const usageColumn = tableInfo.results?.find((column) => column.name === "usage");
|
||||
return (usageColumn?.type || "").toUpperCase();
|
||||
};
|
||||
|
||||
const createApiTokenTable = async (c: Context<HonoCustomType>) => {
|
||||
await c.env.DB.exec(toSingleStatement(`
|
||||
CREATE TABLE api_token (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
usage INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`));
|
||||
};
|
||||
|
||||
const migrateApiTokenUsagePrecision = async (c: Context<HonoCustomType>) => {
|
||||
const hasLegacyTable = await doesTableExist(c, "api_token_legacy_precision");
|
||||
const hasCurrentTable = await doesTableExist(c, "api_token");
|
||||
const usageColumnType = hasCurrentTable ? await getApiTokenUsageColumnType(c) : "";
|
||||
const needsPrecisionMigration = usageColumnType !== "INTEGER";
|
||||
|
||||
if (!hasLegacyTable && !needsPrecisionMigration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceTable = hasLegacyTable ? "api_token_legacy_precision" : "api_token";
|
||||
const legacyTokens = await c.env.DB.prepare(
|
||||
`SELECT key, value, usage, created_at, updated_at FROM ${sourceTable}`
|
||||
).all<LegacyTokenRow>();
|
||||
|
||||
if (!hasLegacyTable && hasCurrentTable) {
|
||||
await c.env.DB.exec(
|
||||
`ALTER TABLE api_token RENAME TO api_token_legacy_precision`
|
||||
);
|
||||
}
|
||||
|
||||
if (await doesTableExist(c, "api_token")) {
|
||||
await c.env.DB.exec(`DROP TABLE IF EXISTS api_token`);
|
||||
}
|
||||
|
||||
await createApiTokenTable(c);
|
||||
|
||||
for (const row of legacyTokens.results || []) {
|
||||
let migratedValue = row.value;
|
||||
|
||||
try {
|
||||
const tokenData = JSON.parse(row.value) as ApiTokenData;
|
||||
migratedValue = JSON.stringify({
|
||||
...tokenData,
|
||||
total_quota: legacyBillingAmountToRaw(tokenData.total_quota),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to migrate token config for ${row.key}:`, error);
|
||||
}
|
||||
|
||||
await c.env.DB.prepare(
|
||||
`INSERT INTO api_token (key, value, usage, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
).bind(
|
||||
row.key,
|
||||
migratedValue,
|
||||
legacyBillingAmountToRaw(row.usage || 0),
|
||||
row.created_at,
|
||||
row.updated_at
|
||||
).run();
|
||||
}
|
||||
|
||||
await c.env.DB.exec(`DROP TABLE IF EXISTS api_token_legacy_precision`);
|
||||
};
|
||||
|
||||
const dbOperations = {
|
||||
initialize: async (c: Context<HonoCustomType>) => {
|
||||
await initializeSchema(c);
|
||||
@@ -44,6 +154,8 @@ const dbOperations = {
|
||||
migrate: async (c: Context<HonoCustomType>) => {
|
||||
await initializeSchema(c);
|
||||
|
||||
await migrateApiTokenUsagePrecision(c);
|
||||
|
||||
const version = await getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version === CONSTANTS.DB_VERSION) {
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user