feat: add agent cost reporting

This commit is contained in:
2026-07-09 20:29:56 +08:00
parent fc5a5365d8
commit 716101eed0
9 changed files with 275 additions and 7 deletions
+64 -1
View File
@@ -73,6 +73,7 @@ function createMockRunAgent(calls: RunRequest[] = []): TestRunner {
status: "completed",
text: "mock response",
usage: { inputTokens: 10, outputTokens: 5 },
costUsd: 0.0023,
numTurns: 1,
sdkSessionId: "sdk-session-1",
};
@@ -128,7 +129,7 @@ describe("trigger full lifecycle (integration)", () => {
msgType: "interactive",
replyInThread: undefined,
});
expect(rt.sentTexts).toContain("mock response");
expect(rt.sentTexts.some((text) => text.includes("mock response") && text.includes("本次成本: $0.0023"))).toBe(true);
expect(runAgentCalls).toHaveLength(1);
expect(runAgentCalls[0]?.providerEnv).toMatchObject({
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
@@ -136,6 +137,9 @@ describe("trigger full lifecycle (integration)", () => {
ANTHROPIC_API_KEY: "",
});
expect(runAgentCalls[0]?.maxTurns).toBe(7);
const run = await prisma.agentRun.findFirstOrThrow();
expect(Number(run.costUsd)).toBeCloseTo(0.0023);
expect(run.costSource).toBe("provider_reported");
});
it("batches quick text messages from the same chat and sender into one run", async () => {
@@ -228,6 +232,65 @@ describe("trigger full lifecycle (integration)", () => {
expect(await prisma.agentRun.findMany()).toHaveLength(0);
});
it("/cost reports recorded current-session cost without creating a run", async () => {
await seedProject("proj-cost", "chat-cost");
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-cost", "@_user_1 写教案"), rt);
await vi.waitFor(async () => {
const runs = await prisma.agentRun.findMany();
expect(runs).toHaveLength(1);
expect(runs[0]?.status).toBe("COMPLETED");
});
await trigger(makeEvent("chat-cost", "@_user_1 /cost"), rt);
const costText = rt.sentTexts.at(-1) ?? "";
expect(costText).toContain("当前会话已记录 agent 成本");
expect(costText).toContain("总计: $0.0023");
expect(costText).toContain("Runs: 1 已记录");
expect(costText).toContain("openrouter / mock-model");
expect(runAgentCalls).toHaveLength(1);
expect(await prisma.agentRun.findMany()).toHaveLength(1);
});
it("/cost surfaces finished runs without recorded cost", async () => {
await seedProject("proj-cost-missing", "chat-cost-missing");
const session = await prisma.agentSession.create({
data: {
projectId: "proj-cost-missing",
provider: "openrouter",
roleId: "draft",
model: "mock-model",
metadata: {},
},
select: { id: true },
});
await prisma.agentRun.create({
data: {
projectId: "proj-cost-missing",
sessionId: session.id,
entrypoint: "FEISHU",
status: "COMPLETED",
prompt: "old test run",
model: "mock-model",
provider: "openrouter",
inputTokens: 10,
outputTokens: 5,
metadata: {},
finishedAt: new Date(),
},
});
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
await trigger(makeEvent("chat-cost-missing", "@_user_1 /cost"), rt);
const costText = rt.sentTexts.at(-1) ?? "";
expect(costText).toContain("还没有任何 run 记录到真实成本");
expect(costText).toContain("未记录成本: 1 runs");
expect(runAgentCalls).toHaveLength(0);
});
it("/help is built from the current registry on each request", async () => {
await seedProject("proj-help-live", "chat-help-live");
let currentModels = new InMemoryModelRegistry(