feat(billing): record prompt cache usage

This commit is contained in:
Jesse_Chen
2026-08-31 09:59:53 +08:00
parent a499c3444e
commit 3a762e1fca
12 changed files with 180 additions and 30 deletions
+26 -7
View File
@@ -29,6 +29,7 @@ import {
shouldLoadGeneralDailyPanchanga,
} from "@/lib/consultation-entrypoint";
import { CreditRpcError } from "@/lib/consultation-billing";
import { cachedSystemMessage, mergePromptCacheUsage, promptCacheUsage } from "@/lib/agent-generation-settings";
import { resolveFeaturePricing } from "@/lib/feature-pricing";
import { reserveConsultationModel } from "@/lib/consultation-model-selection";
import { resolveSessionLanguageModel } from "@/lib/model-catalog";
@@ -203,13 +204,22 @@ function chinaCalendarDate(now: Date) {
return new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString().slice(0, 10);
}
type Usage = { inputTokens?: number; outputTokens?: number };
type Usage = {
inputTokens?: number;
outputTokens?: number;
cache?: ReturnType<typeof promptCacheUsage>;
};
function mergeUsage(usages: Promise<Usage>[]): Promise<Usage> {
return Promise.all(usages).then((items) => items.reduce((total, item) => ({
inputTokens: (total.inputTokens ?? 0) + (item.inputTokens ?? 0),
outputTokens: (total.outputTokens ?? 0) + (item.outputTokens ?? 0),
}), {} as Usage));
return Promise.all(usages).then((items) => items.reduce<Usage>((total, item) => {
const usage = item && typeof item === "object" ? item as Record<string, unknown> : {};
const cache = promptCacheUsage(usage);
return {
inputTokens: (total.inputTokens ?? 0) + (typeof usage.inputTokens === "number" ? usage.inputTokens : 0),
outputTokens: (total.outputTokens ?? 0) + (typeof usage.outputTokens === "number" ? usage.outputTokens : 0),
cache: mergePromptCacheUsage([total.cache, cache]),
};
}, {} as Usage));
}
function shouldUseAgenticRuntime(user: { id: string; app_metadata?: Record<string, unknown> }) {
@@ -484,8 +494,10 @@ export async function POST(request: Request) {
const usageStartedAt = Date.now();
async function usagePayload(usage: Promise<{ inputTokens?: number; outputTokens?: number }>) {
const resolved = await usage;
const inputTokens = Math.max(0, Math.trunc(resolved.inputTokens ?? 0));
const outputTokens = Math.max(0, Math.trunc(resolved.outputTokens ?? 0));
const usageRecord = resolved as Record<string, unknown>;
const cache = promptCacheUsage(usageRecord);
const inputTokens = Math.max(0, Math.trunc(typeof usageRecord.inputTokens === "number" ? usageRecord.inputTokens : 0));
const outputTokens = Math.max(0, Math.trunc(typeof usageRecord.outputTokens === "number" ? usageRecord.outputTokens : 0));
return {
eventKey: requestId,
actualModelId: selectedModel.id,
@@ -497,6 +509,7 @@ export async function POST(request: Request) {
+ outputTokens * (selectedModel.outputCostMicrousdPerMillion ?? 0)
) / 1_000_000),
durationMs: Date.now() - usageStartedAt,
...(cache ? { metadata: { cache: { ...cache, hit: cache.readTokens > 0 } } } : {}),
};
}
@@ -674,7 +687,9 @@ export async function POST(request: Request) {
// failure response must not depend on it succeeding.
}
};
const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model);
const baseMessages = [
...(cacheBoundary ? [cacheBoundary] : []),
...history.map((message) => message.role === "user"
? { role: "user" as const, content: message.text }
: { role: "assistant" as const, content: message.text }),
@@ -1033,7 +1048,9 @@ export async function POST(request: Request) {
return await runAgenticConsultation(consultationMode, history, name, generalDailyContext);
}
if (!shouldRunBirthChartWorkflow(consultationMode)) {
const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model);
const result = await getGeneralJyotishAgent(selectedModel).stream([
...(cacheBoundary ? [cacheBoundary] : []),
{
role: "user",
content: [
@@ -1107,7 +1124,9 @@ export async function POST(request: Request) {
);
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model);
const result = await getLegacyJyotishAgent(selectedModel, workflowContext).stream([
...(cacheBoundary ? [cacheBoundary] : []),
...history.map((message) => message.role === "user"
? { role: "user" as const, content: message.text }
: { role: "assistant" as const, content: message.text }),
@@ -594,6 +594,7 @@ export async function POST(request: Request) {
modelConfigVersion: selectedModel.configVersion,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
...(usage.cache ? { metadata: { cache: { ...usage.cache, hit: usage.cache.readTokens > 0 } } } : {}),
costMicrousd: Math.round((
usage.inputTokens * (selectedModel.inputCostMicrousdPerMillion ?? 0)
+ usage.outputTokens * (selectedModel.outputCostMicrousdPerMillion ?? 0)
+1
View File
@@ -215,6 +215,7 @@ export async function POST(request: Request) {
eventKey: "report.full", actualModelId: usage.actualModelId,
modelConfigVersion: usage.modelConfigVersion, inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens, costMicrousd, durationMs: usage.durationMs,
...(usage.cache ? { metadata: { cache: { ...usage.cache, hit: usage.cache.readTokens > 0 } } } : {}),
});
return settled.success;
},